From 9fb0c73e0ab67ed9504c34000f46c0af2d5ce9d1 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 19:30:04 -0700 Subject: [PATCH 1/7] Raise gemini-local hello probe timeout to 60s for SSH and E2B targets (#5322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The Gemini adapter's environment Test surfaces a hello probe so operators can confirm the CLI runs end-to-end on the configured target > - On SSH and E2B sandbox targets the round-trip cost (login-shell sourcing, network, model warm-up) routinely exceeds the existing 10s probe timeout, so the probe spuriously fails on environments that are actually healthy > - This pull request raises the gemini-local hello probe timeout to 60s, matching the timeout we use for slower-bootstrapping adapters > - The benefit is the Gemini Test action no longer reports false negatives on remote targets that need a longer first-run window ## What Changed - `packages/adapters/gemini-local/src/server/test.ts`: hello probe timeout raised from 10s to 60s ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-gemini-local` - Manual: SSH and E2B Gemini hello probes now complete cleanly without spurious timeouts ## Risks Low. A 60s ceiling on a non-blocking probe is consistent with sibling adapters; the only behavior change is a longer worst-case wait when the probe genuinely hangs. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — N/A (one-line timeout change) - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- packages/adapters/gemini-local/src/server/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapters/gemini-local/src/server/test.ts b/packages/adapters/gemini-local/src/server/test.ts index 901e5b862bc..8c8bf007c5f 100644 --- a/packages/adapters/gemini-local/src/server/test.ts +++ b/packages/adapters/gemini-local/src/server/test.ts @@ -167,7 +167,7 @@ export async function testEnvironment( const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim(); const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default"); const sandbox = asBoolean(config.sandbox, false); - const helloProbeTimeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, 10)); + const helloProbeTimeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, 60)); const extraArgs = (() => { const fromExtraArgs = asStringArray(config.extraArgs); if (fromExtraArgs.length > 0) return fromExtraArgs; From 83e7ecc58e6206c368a1356c30656611b3903d85 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 19:30:08 -0700 Subject: [PATCH 2/7] Preserve scope on manual heartbeat invokes (#5323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The agent live-run route lets operators trigger a manual heartbeat invocation so an agent can pick up a specific issue or step out of band > - The current route flow drops the caller's scope (issue/run context) when forwarding the manual invoke into the heartbeat service, so the resulting run loses the targeting the operator specified > - This pull request threads the operator-supplied scope through the manual invoke path on both the server route and the UI client, with a regression test that confirms the scope round-trips > - The benefit is manual heartbeat invokes from the live-run UI actually pick up the scoped issue/run instead of falling through to the agent's default routine ## What Changed - `server/src/routes/agents.ts`: forward the operator-supplied scope into the manual invoke heartbeat service call - `server/src/__tests__/agent-live-run-routes.test.ts`: new test verifying the manual invoke path preserves scope - `ui/src/api/agents.ts`: pass scope through the live-run client API ## Verification - `pnpm vitest run --no-coverage server/src/__tests__/agent-live-run-routes.test.ts` - `pnpm typecheck` clean ## Risks Low. The change is purely additive on the route surface — handlers that did not previously pass scope continue to work; handlers that did pass it now have it preserved instead of dropped. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — new test covers the preserved-scope path - [x] If this change affects the UI, I have included before/after screenshots — N/A (internal API change, no visible UI shift) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../__tests__/agent-live-run-routes.test.ts | 73 +++++++++++++++++ server/src/routes/agents.ts | 82 +++++++++++++++---- ui/src/api/agents.ts | 20 +++-- 3 files changed, 151 insertions(+), 24 deletions(-) diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index 470ef85d5eb..0eeebe524f1 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -12,6 +12,7 @@ const mockHeartbeatService = vi.hoisted(() => ({ getActiveRunIssueSummaryForAgent: vi.fn(), getRunLogAccess: vi.fn(), readLog: vi.fn(), + wakeup: vi.fn(), })); const mockIssueService = vi.hoisted(() => ({ @@ -26,6 +27,8 @@ const mockInstanceSettingsService = vi.hoisted(() => ({ listCompanyIds: vi.fn(), })); +const routeAgentId = "11111111-1111-4111-8111-111111111111"; + function registerModuleMocks() { vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js")); @@ -210,6 +213,14 @@ describe("agent live run routes", () => { content: "chunk", nextOffset: 5, }); + mockHeartbeatService.wakeup.mockResolvedValue({ + id: "run-1", + companyId: "company-1", + agentId: "agent-1", + status: "queued", + invocationSource: "on_demand", + triggerDetail: "manual", + }); }); it("returns a compact active run payload for issue polling", async () => { @@ -524,4 +535,66 @@ describe("agent live run routes", () => { expect(res.body).toHaveLength(4); expect(db.select).toHaveBeenCalledTimes(2); }); + + it("passes scoped wake fields through the legacy heartbeat invoke route", async () => { + const res = await requestApp( + await createApp(), + (baseUrl) => request(baseUrl) + .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) + .send({ + reason: "issue_assigned", + payload: { + issueId: "issue-1", + taskId: "issue-1", + taskKey: "issue-1", + }, + forceFreshSession: true, + }), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(202); + // The legacy /heartbeat/invoke endpoint forwards only the wake fields the + // caller actually supplied so empty-body callers (e.g. e2e suites) match + // the original fixed-arg `heartbeat.invoke()` shape exactly. When the + // caller supplies reason / payload / forceFreshSession those are + // forwarded; idempotencyKey is omitted unless explicitly set. + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, { + source: "on_demand", + triggerDetail: "manual", + reason: "issue_assigned", + payload: { + issueId: "issue-1", + taskId: "issue-1", + taskKey: "issue-1", + }, + requestedByActorType: "user", + requestedByActorId: "local-board", + contextSnapshot: { + triggeredBy: "board", + actorId: "local-board", + forceFreshSession: true, + }, + }); + }); + + it("calls heartbeat.wakeup with the legacy minimal shape when the body is empty", async () => { + const res = await requestApp( + await createApp(), + (baseUrl) => request(baseUrl) + .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) + .send({}), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(202); + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, { + source: "on_demand", + triggerDetail: "manual", + requestedByActorType: "user", + requestedByActorId: "local-board", + contextSnapshot: { + triggeredBy: "board", + actorId: "local-board", + }, + }); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index e104d8b0072..ce830140e42 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2883,7 +2883,25 @@ export function agentRoutes( res.json({ ok: true }); }); - router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => { + // Shared handler body for the wakeup-style endpoints. The two routes differ + // only in: + // - `source` — the modern /wakeup endpoint reads it from the request body + // (timer|assignment|on_demand|automation) while the legacy + // /heartbeat/invoke endpoint hardcodes "on_demand", since it has only + // ever produced on-demand invocations. + // - skipped-response shape — the modern endpoint surfaces the rich + // SkippedWakeupResponse; the legacy endpoint stays on the simpler + // { status: "skipped" } shape for backward compat. + type HeartbeatSource = "timer" | "assignment" | "on_demand" | "automation"; + type WakeupRouteOpts = { + source: HeartbeatSource | undefined; + skippedResponse: (agent: NonNullable>>) => unknown | Promise; + }; + const handleWakeupRoute = async ( + req: Request, + res: Response, + opts: WakeupRouteOpts, + ): Promise => { const id = req.params.id as string; const agent = await svc.getById(id); if (!agent) { @@ -2902,7 +2920,7 @@ export function agentRoutes( } const run = await heartbeat.wakeup(id, { - source: req.body.source, + source: opts.source, triggerDetail: req.body.triggerDetail ?? "manual", reason: req.body.reason ?? null, payload: req.body.payload ?? null, @@ -2917,7 +2935,7 @@ export function agentRoutes( }); if (!run) { - res.status(202).json(await buildSkippedWakeupResponse(agent, req.body.payload ?? null)); + res.status(202).json(await opts.skippedResponse(agent)); return; } @@ -2935,9 +2953,23 @@ export function agentRoutes( }); res.status(202).json(run); + }; + + router.post("/agents/:id/wakeup", validate(wakeAgentSchema), async (req, res) => { + await handleWakeupRoute(req, res, { + source: req.body.source, + skippedResponse: (agent) => buildSkippedWakeupResponse(agent, req.body.payload ?? null), + }); }); router.post("/agents/:id/heartbeat/invoke", async (req, res) => { + // Legacy endpoint. Hardcodes `source: "on_demand"` (the prior behavior + // before the wakeup/invoke convergence). Reads scope fields directly off + // the body without `validate(wakeAgentSchema)` because callers — including + // the e2e suite — post an empty body, and the schema rejects undefined + // / missing bodies. Only forwards fields the caller actually supplied so + // an empty body produces the original fixed-arg `heartbeat.invoke()` + // shape exactly. const id = req.params.id as string; const agent = await svc.getById(id); if (!agent) { @@ -2955,19 +2987,37 @@ export function agentRoutes( await assertBoardCanManageAgentsForCompany(req, agent.companyId); } - const run = await heartbeat.invoke( - id, - "on_demand", - { - triggeredBy: req.actor.type, - actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, - }, - "manual", - { - actorType: req.actor.type === "agent" ? "agent" : "user", - actorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null, - }, - ); + const body = (req.body ?? {}) as Partial<{ + reason: unknown; + payload: unknown; + idempotencyKey: unknown; + forceFreshSession: unknown; + triggerDetail: unknown; + }>; + const contextSnapshot: Record = { + triggeredBy: req.actor.type, + actorId: req.actor.type === "agent" ? req.actor.agentId : req.actor.userId, + }; + if (body.forceFreshSession === true) { + contextSnapshot.forceFreshSession = true; + } + const wakeOpts: Parameters[1] = { + source: "on_demand", + triggerDetail: typeof body.triggerDetail === "string" ? body.triggerDetail as "manual" | "system" | "ping" | "callback" : "manual", + requestedByActorType: req.actor.type === "agent" ? "agent" : "user", + requestedByActorId: req.actor.type === "agent" ? req.actor.agentId ?? null : req.actor.userId ?? null, + contextSnapshot, + }; + if (typeof body.reason === "string" && body.reason.length > 0) { + wakeOpts.reason = body.reason; + } + if (body.payload && typeof body.payload === "object" && !Array.isArray(body.payload)) { + wakeOpts.payload = body.payload as Record; + } + if (typeof body.idempotencyKey === "string" && body.idempotencyKey.length > 0) { + wakeOpts.idempotencyKey = body.idempotencyKey; + } + const run = await heartbeat.wakeup(id, wakeOpts); if (!run) { res.status(202).json({ status: "skipped" }); diff --git a/ui/src/api/agents.ts b/ui/src/api/agents.ts index f25aa5b30e4..6478ec754bd 100644 --- a/ui/src/api/agents.ts +++ b/ui/src/api/agents.ts @@ -69,6 +69,15 @@ export interface AgentPermissionUpdate { canAssignTasks: boolean; } +export interface AgentWakeRequest { + source?: "timer" | "assignment" | "on_demand" | "automation"; + triggerDetail?: "manual" | "ping" | "callback" | "system"; + reason?: string | null; + payload?: Record | null; + idempotencyKey?: string | null; + forceFreshSession?: boolean; +} + function withCompanyScope(path: string, companyId?: string) { if (!companyId) return path; const separator = path.includes("?") ? "&" : "?"; @@ -204,16 +213,11 @@ export const agentsApi = { `/companies/${companyId}/adapters/${type}/test-environment`, data, ), - invoke: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/heartbeat/invoke"), {}), + invoke: (id: string, companyId?: string, data: AgentWakeRequest = {}) => + api.post(agentPath(id, companyId, "/heartbeat/invoke"), data), wakeup: ( id: string, - data: { - source?: "timer" | "assignment" | "on_demand" | "automation"; - triggerDetail?: "manual" | "ping" | "callback" | "system"; - reason?: string | null; - payload?: Record | null; - idempotencyKey?: string | null; - }, + data: AgentWakeRequest, companyId?: string, ) => api.post(agentPath(id, companyId, "/wakeup"), data), loginWithClaude: (id: string, companyId?: string) => From 36eaf9778fa68eb19e7c9c5cbe65b436b6b8f2a1 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 19:30:11 -0700 Subject: [PATCH 3/7] Expand sandbox callback bridge allowlist to cover the documented heartbeat surface (#5324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - When an agent runs in an e2b sandbox or other non-managed environment, it talks back to the Paperclip server through a per-lease callback bridge that proxies HTTP requests > - The bridge has an allowlist of method/path patterns it will forward; anything outside the list is rejected to keep the bridge tight > - The allowlist had drifted behind what the heartbeat documentation describes as the supported callback surface — several documented endpoints (issue updates, agent-side log emit, work-status writes) were being rejected at the bridge > - This pull request expands the allowlist to cover the documented heartbeat surface and adds tests that pin every newly-allowed pattern, so the doc and the bridge stay in sync > - The benefit is sandboxed runs no longer hit "method not allowed" / "path not allowed" rejections on the documented set of callbacks ## What Changed - `packages/adapter-utils/src/sandbox-callback-bridge.ts`: expand the method/path allowlist to match the documented heartbeat callback surface - `packages/adapter-utils/src/sandbox-callback-bridge.test.ts`: add coverage for every newly-allowed pattern, plus negative cases for patterns that should still be rejected ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-utils` - `pnpm typecheck` clean - Manual: previously-rejected callbacks from sandboxed runs now succeed end-to-end ## Risks Low. The allowlist only grows; nothing previously allowed is now blocked. Tests pin both the new allowed patterns and that out-of-doc patterns stay rejected. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — new tests cover added patterns + still-rejected negatives - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../src/sandbox-callback-bridge.test.ts | 92 +++++++++++++++++++ .../src/sandbox-callback-bridge.ts | 67 +++++++++++++- 2 files changed, 156 insertions(+), 3 deletions(-) diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts index dc04df6c5dc..c18343afd66 100644 --- a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts +++ b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { prepareCommandManagedRuntime } from "./command-managed-runtime.js"; import { + authorizeSandboxCallbackBridgeRequestWithRoutes, createFileSystemSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, @@ -613,4 +614,95 @@ describe("sandbox callback bridge", () => { error: expect.stringMatching(/JSON|Unexpected|Unterminated/i), }); }); + + it("permits the documented heartbeat surface and denies unrelated routes", () => { + const allowed: Array<{ method: string; path: string }> = [ + { method: "GET", path: "/api/agents/me" }, + { method: "GET", path: "/api/agents/me/inbox-lite" }, + { method: "GET", path: "/api/agents/me/inbox/mine" }, + { method: "GET", path: "/api/agents/agent-1" }, + { method: "GET", path: "/api/agents/agent-1/skills" }, + { method: "POST", path: "/api/agents/agent-1/skills/sync" }, + { method: "PATCH", path: "/api/agents/agent-1/instructions-path" }, + { method: "GET", path: "/api/companies/co-1" }, + { method: "GET", path: "/api/companies/co-1/dashboard" }, + { method: "GET", path: "/api/companies/co-1/agents" }, + { method: "GET", path: "/api/companies/co-1/issues" }, + { method: "GET", path: "/api/companies/co-1/projects" }, + { method: "GET", path: "/api/companies/co-1/goals" }, + { method: "GET", path: "/api/companies/co-1/org" }, + { method: "GET", path: "/api/companies/co-1/approvals" }, + { method: "GET", path: "/api/companies/co-1/routines" }, + { method: "GET", path: "/api/companies/co-1/skills" }, + { method: "GET", path: "/api/projects/proj-1" }, + { method: "GET", path: "/api/goals/goal-1" }, + { method: "GET", path: "/api/issues/issue-1" }, + { method: "GET", path: "/api/issues/issue-1/heartbeat-context" }, + { method: "GET", path: "/api/issues/issue-1/comments" }, + { method: "GET", path: "/api/issues/issue-1/comments/c-1" }, + { method: "POST", path: "/api/issues/issue-1/comments" }, + { method: "GET", path: "/api/issues/issue-1/documents" }, + { method: "GET", path: "/api/issues/issue-1/documents/plan" }, + { method: "GET", path: "/api/issues/issue-1/documents/plan/revisions" }, + { method: "PUT", path: "/api/issues/issue-1/documents/plan" }, + { method: "POST", path: "/api/issues/issue-1/checkout" }, + { method: "POST", path: "/api/issues/issue-1/release" }, + { method: "PATCH", path: "/api/issues/issue-1" }, + { method: "GET", path: "/api/issues/issue-1/approvals" }, + { method: "GET", path: "/api/issues/issue-1/interactions" }, + { method: "GET", path: "/api/issues/issue-1/interactions/inter-1" }, + { method: "POST", path: "/api/issues/issue-1/interactions" }, + { method: "POST", path: "/api/issues/issue-1/interactions/inter-1/accept" }, + { method: "POST", path: "/api/issues/issue-1/interactions/inter-1/reject" }, + { method: "POST", path: "/api/issues/issue-1/interactions/inter-1/respond" }, + { method: "POST", path: "/api/companies/co-1/issues" }, + { method: "GET", path: "/api/approvals/ap-1" }, + { method: "GET", path: "/api/approvals/ap-1/issues" }, + { method: "GET", path: "/api/approvals/ap-1/comments" }, + { method: "POST", path: "/api/approvals/ap-1/comments" }, + { method: "POST", path: "/api/companies/co-1/approvals" }, + { method: "GET", path: "/api/execution-workspaces/ws-1" }, + { method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/start" }, + { method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/stop" }, + { method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/restart" }, + { method: "GET", path: "/api/routines/r-1" }, + { method: "GET", path: "/api/routines/r-1/runs" }, + { method: "POST", path: "/api/companies/co-1/routines" }, + { method: "PATCH", path: "/api/routines/r-1" }, + { method: "POST", path: "/api/routines/r-1/run" }, + { method: "POST", path: "/api/routines/r-1/triggers" }, + { method: "PATCH", path: "/api/routine-triggers/t-1" }, + { method: "DELETE", path: "/api/routine-triggers/t-1" }, + ]; + for (const request of allowed) { + expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBeNull(); + } + + const denied: Array<{ method: string; path: string }> = [ + { method: "DELETE", path: "/api/secrets" }, + // Pin the runtime-services regex to start/stop/restart only — anything + // else (delete, reset, wipe, etc.) must stay denied even if the API + // grows new actions later. + { method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/delete" }, + { method: "POST", path: "/api/companies/co-1/agents" }, + { method: "POST", path: "/api/agents/agent-1/pause" }, + { method: "POST", path: "/api/agents/agent-1/terminate" }, + { method: "POST", path: "/api/agents/agent-1/keys" }, + { method: "POST", path: "/api/companies/co-1/exports" }, + { method: "POST", path: "/api/companies/co-1/imports/apply" }, + { method: "POST", path: "/api/companies/co-1/archive" }, + { method: "DELETE", path: "/api/issues/issue-1/documents/plan" }, + { method: "DELETE", path: "/api/issues/issue-1/approvals/ap-1" }, + { method: "POST", path: "/api/approvals/ap-1/approve" }, + { method: "POST", path: "/api/approvals/ap-1/reject" }, + { method: "POST", path: "/api/companies/co-1/logo" }, + { method: "GET", path: "/api/companies/co-1/secrets" }, + { method: "PATCH", path: "/api/secrets/secret-1" }, + ]; + for (const request of denied) { + expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBe( + `Route not allowed: ${request.method} ${request.path}`, + ); + } + }); }); diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.ts b/packages/adapter-utils/src/sandbox-callback-bridge.ts index 666ba9e925e..cb020046c13 100644 --- a/packages/adapter-utils/src/sandbox-callback-bridge.ts +++ b/packages/adapter-utils/src/sandbox-callback-bridge.ts @@ -23,15 +23,76 @@ export interface SandboxCallbackBridgeRouteRule { path: RegExp; } +// Routes the in-sandbox heartbeat skill is documented to call. The server +// still enforces actor-level permissions on top of this allowlist; the list +// exists to bound the surface area a compromised CLI could reach via the +// reverse bridge. Keep this in sync with the Paperclip skill in +// `skills/paperclip/SKILL.md` and `references/api-reference.md`. export const DEFAULT_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST: readonly SandboxCallbackBridgeRouteRule[] = [ + // Identity, inbox, agent self-management { method: "GET", path: /^\/api\/agents\/me$/ }, + { method: "GET", path: /^\/api\/agents\/me\/inbox-lite$/ }, + { method: "GET", path: /^\/api\/agents\/me\/inbox\/mine$/ }, + { method: "GET", path: /^\/api\/agents\/[^/]+$/ }, + { method: "GET", path: /^\/api\/agents\/[^/]+\/skills$/ }, + { method: "POST", path: /^\/api\/agents\/[^/]+\/skills\/sync$/ }, + { method: "PATCH", path: /^\/api\/agents\/[^/]+\/instructions-path$/ }, + + // Company-level reads used to discover work and context + { method: "GET", path: /^\/api\/companies\/[^/]+$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/dashboard$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/agents$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/issues$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/projects$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/goals$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/org$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/approvals$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/routines$/ }, + { method: "GET", path: /^\/api\/companies\/[^/]+\/skills$/ }, + { method: "GET", path: /^\/api\/projects\/[^/]+$/ }, + { method: "GET", path: /^\/api\/goals\/[^/]+$/ }, + + // Issue lifecycle: read context, checkout, update, comment, document, release + { method: "GET", path: /^\/api\/issues\/[^/]+$/ }, { method: "GET", path: /^\/api\/issues\/[^/]+\/heartbeat-context$/ }, { method: "GET", path: /^\/api\/issues\/[^/]+\/comments(?:\/[^/]+)?$/ }, - { method: "GET", path: /^\/api\/issues\/[^/]+\/documents(?:\/[^/]+)?$/ }, - { method: "POST", path: /^\/api\/issues\/[^/]+\/checkout$/ }, { method: "POST", path: /^\/api\/issues\/[^/]+\/comments$/ }, - { method: "POST", path: /^\/api\/issues\/[^/]+\/interactions(?:\/[^/]+)?$/ }, + { method: "GET", path: /^\/api\/issues\/[^/]+\/documents(?:\/[^/]+)?$/ }, + { method: "GET", path: /^\/api\/issues\/[^/]+\/documents\/[^/]+\/revisions$/ }, + { method: "PUT", path: /^\/api\/issues\/[^/]+\/documents\/[^/]+$/ }, + { method: "POST", path: /^\/api\/issues\/[^/]+\/checkout$/ }, + { method: "POST", path: /^\/api\/issues\/[^/]+\/release$/ }, { method: "PATCH", path: /^\/api\/issues\/[^/]+$/ }, + { method: "GET", path: /^\/api\/issues\/[^/]+\/approvals$/ }, + + // Issue-thread interactions (suggest tasks, ask questions, request confirmation) + { method: "GET", path: /^\/api\/issues\/[^/]+\/interactions(?:\/[^/]+)?$/ }, + { method: "POST", path: /^\/api\/issues\/[^/]+\/interactions$/ }, + { method: "POST", path: /^\/api\/issues\/[^/]+\/interactions\/[^/]+\/(?:accept|reject|respond)$/ }, + + // Subtasks / delegation + { method: "POST", path: /^\/api\/companies\/[^/]+\/issues$/ }, + + // Approvals (request, read, comment) + { method: "GET", path: /^\/api\/approvals\/[^/]+$/ }, + { method: "GET", path: /^\/api\/approvals\/[^/]+\/issues$/ }, + { method: "GET", path: /^\/api\/approvals\/[^/]+\/comments$/ }, + { method: "POST", path: /^\/api\/approvals\/[^/]+\/comments$/ }, + { method: "POST", path: /^\/api\/companies\/[^/]+\/approvals$/ }, + + // Execution workspaces and runtime services (start/stop/restart dev servers) + { method: "GET", path: /^\/api\/execution-workspaces\/[^/]+$/ }, + { method: "POST", path: /^\/api\/execution-workspaces\/[^/]+\/runtime-services\/(?:start|stop|restart)$/ }, + + // Routines (agents manage their own routines and triggers) + { method: "GET", path: /^\/api\/routines\/[^/]+$/ }, + { method: "GET", path: /^\/api\/routines\/[^/]+\/runs$/ }, + { method: "POST", path: /^\/api\/companies\/[^/]+\/routines$/ }, + { method: "PATCH", path: /^\/api\/routines\/[^/]+$/ }, + { method: "POST", path: /^\/api\/routines\/[^/]+\/run$/ }, + { method: "POST", path: /^\/api\/routines\/[^/]+\/triggers$/ }, + { method: "PATCH", path: /^\/api\/routine-triggers\/[^/]+$/ }, + { method: "DELETE", path: /^\/api\/routine-triggers\/[^/]+$/ }, ] as const; export const DEFAULT_SANDBOX_CALLBACK_BRIDGE_HEADER_ALLOWLIST = [ From f6bad8f6bf95848e481098016433280f6508a1e2 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 19:30:14 -0700 Subject: [PATCH 4/7] Sanitize remote execution envs at the boundary (#5325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Adapters spawn CLIs against local, SSH, and sandbox targets, threading a runtime env through `runAdapterExecutionTargetProcess` and the SSH/sandbox runners > - Host identity vars (HOME, TMPDIR, XDG_*, NVM_DIR, PATH) routinely leak into the env we send to remote targets — sometimes via test probes, sometimes via runtime config — and break sandboxed/SSH'd CLIs whose own profiles set those values correctly > - The sanitization logic existed but lived alongside other helpers in `server-utils.ts` and was applied piecemeal at adapter callsites, so it was easy to bypass > - This pull request lifts the sanitization into a standalone `remote-execution-env.ts`, applies it at the SSH and sandbox runtime boundary so every remote spawn goes through it, and removes the duplicated callsite-level filtering > - The benefit is identity-bound host env stops leaking across SSH/sandbox transports regardless of which adapter calls in ## What Changed - `packages/adapter-utils/src/remote-execution-env.ts`: new module — single source of truth for which env keys are identity-bound and how to strip them when the value matches the host's value - `packages/adapter-utils/src/server-utils.ts`: remove the inline sanitization (now in `remote-execution-env.ts`) - `packages/adapter-utils/src/execution-target.ts`: apply sanitization at the sandbox runtime boundary - `packages/adapter-utils/src/ssh.ts`: apply sanitization at the SSH spawn boundary - `packages/adapters/opencode-local/src/server/test.ts`: drop now-redundant callsite filtering - `packages/adapters/pi-local/src/server/test.ts`: drop now-redundant callsite filtering - New tests `execution-target.test.ts` and `execution-target-sandbox.test.ts` cover the sanitizer flow at both transports, including positive cases (host-shaped path stripped) and explicit-override preservation ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-utils --project @paperclipai/adapter-opencode-local --project @paperclipai/adapter-pi-local` - `pnpm typecheck` clean ## Risks Low–medium. The sanitization is now applied at one layer (boundary) instead of N (callsites), so behavior is more consistent. Any adapter that previously relied on a leaked host var landing on the remote shell would now see it stripped — but those reliances were what this change exists to fix. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — new tests at both transports - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../src/execution-target-sandbox.test.ts | 87 +++++++++++++ .../src/execution-target.test.ts | 122 +++++++++++++++++- .../adapter-utils/src/execution-target.ts | 24 +++- .../adapter-utils/src/remote-execution-env.ts | 49 +++++++ packages/adapter-utils/src/server-utils.ts | 50 +------ packages/adapter-utils/src/ssh.ts | 24 +++- .../opencode-local/src/server/test.ts | 9 +- packages/adapters/pi-local/src/server/test.ts | 13 +- 8 files changed, 306 insertions(+), 72 deletions(-) create mode 100644 packages/adapter-utils/src/remote-execution-env.ts diff --git a/packages/adapter-utils/src/execution-target-sandbox.test.ts b/packages/adapter-utils/src/execution-target-sandbox.test.ts index e0ebb37cc50..91bfd57d0d1 100644 --- a/packages/adapter-utils/src/execution-target-sandbox.test.ts +++ b/packages/adapter-utils/src/execution-target-sandbox.test.ts @@ -19,6 +19,7 @@ describe("sandbox adapter execution targets", () => { const cleanupDirs: string[] = []; afterEach(async () => { + vi.unstubAllEnvs(); while (cleanupDirs.length > 0) { const dir = cleanupDirs.pop(); if (!dir) continue; @@ -141,6 +142,92 @@ describe("sandbox adapter execution targets", () => { })); }); + it("strips inherited host identity env before sandbox execution", async () => { + vi.stubEnv("PATH", "/host/bin:/usr/bin"); + vi.stubEnv("HOME", "/Users/local"); + vi.stubEnv("TMPDIR", "/var/folders/local/T"); + + const runner = { + execute: vi.fn(async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "ok\n", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + })), + }; + const target: AdapterSandboxExecutionTarget = { + kind: "remote", + transport: "sandbox", + remoteCwd: "/workspace", + runner, + }; + + await runAdapterExecutionTargetProcess("run-1b", target, "agent-cli", ["--json"], { + cwd: "/local/workspace", + env: { + PATH: "/host/bin:/usr/bin", + HOME: "/Users/local", + TMPDIR: "/var/folders/local/T", + SAFE_VALUE: "visible", + }, + timeoutSec: 5, + graceSec: 1, + onLog: async () => {}, + }); + + expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({ + env: { + SAFE_VALUE: "visible", + }, + })); + }); + + it("preserves explicit remote identity env overrides for sandbox execution", async () => { + vi.stubEnv("PATH", "/host/bin:/usr/bin"); + vi.stubEnv("HOME", "/Users/local"); + + const runner = { + execute: vi.fn(async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "ok\n", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + })), + }; + const target: AdapterSandboxExecutionTarget = { + kind: "remote", + transport: "sandbox", + remoteCwd: "/workspace", + runner, + }; + + await runAdapterExecutionTargetProcess("run-1c", target, "agent-cli", ["--json"], { + cwd: "/local/workspace", + env: { + PATH: "/custom/remote/bin:/usr/bin", + HOME: "/home/sandbox", + SAFE_VALUE: "visible", + }, + timeoutSec: 5, + graceSec: 1, + onLog: async () => {}, + }); + + expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({ + env: { + PATH: "/custom/remote/bin:/usr/bin", + HOME: "/home/sandbox", + SAFE_VALUE: "visible", + }, + })); + }); + it("treats SSH targets as bridge-only", () => { const target = { kind: "remote" as const, diff --git a/packages/adapter-utils/src/execution-target.test.ts b/packages/adapter-utils/src/execution-target.test.ts index 71bcc34499d..22b04ab8cb6 100644 --- a/packages/adapter-utils/src/execution-target.test.ts +++ b/packages/adapter-utils/src/execution-target.test.ts @@ -1,15 +1,18 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import * as ssh from "./ssh.js"; +import * as serverUtils from "./server-utils.js"; import { adapterExecutionTargetUsesManagedHome, ensureAdapterExecutionTargetRuntimeCommandInstalled, resolveAdapterExecutionTargetCwd, + runAdapterExecutionTargetProcess, runAdapterExecutionTargetShellCommand, } from "./execution-target.js"; describe("runAdapterExecutionTargetShellCommand", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); }); it("quotes remote shell commands with the shared SSH quoting helper", async () => { @@ -42,16 +45,68 @@ describe("runAdapterExecutionTargetShellCommand", () => { }, ); + // runSshCommand owns profile sourcing and the outer `sh -lc` wrapper — + // the caller passes the raw command string. Wrapping it here would + // double-nest the login shell and re-source profiles after the explicit + // env override, silently undoing identity-var preservation. expect(runSshCommandSpy).toHaveBeenCalledWith( expect.objectContaining({ host: "ssh.example.test", username: "ssh-user", }), - `sh -lc ${ssh.shellQuote(`printf '%s\\n' "$HOME" && echo "it's ok"`)}`, + `printf '%s\\n' "$HOME" && echo "it's ok"`, expect.any(Object), ); }); + it("sanitizes inherited host env before SSH shell execution", async () => { + vi.stubEnv("PATH", "/host/bin:/usr/bin"); + vi.stubEnv("HOME", "/Users/local"); + + const runSshCommandSpy = vi.spyOn(ssh, "runSshCommand").mockResolvedValue({ + stdout: "", + stderr: "", + }); + + await runAdapterExecutionTargetShellCommand( + "run-1b", + { + kind: "remote", + transport: "ssh", + remoteCwd: "/srv/paperclip/workspace", + spec: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteCwd: "/srv/paperclip/workspace", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }, + "env", + { + cwd: "/tmp/local", + env: { + PATH: "/host/bin:/usr/bin", + HOME: "/Users/local", + SAFE_VALUE: "visible", + }, + }, + ); + + expect(runSshCommandSpy).toHaveBeenCalledWith( + expect.any(Object), + expect.any(String), + expect.objectContaining({ + env: { + SAFE_VALUE: "visible", + }, + }), + ); + }); + it("returns a timedOut result when the SSH shell command times out", async () => { vi.spyOn(ssh, "runSshCommand").mockRejectedValue(Object.assign(new Error("timed out"), { code: "ETIMEDOUT", @@ -162,6 +217,71 @@ describe("runAdapterExecutionTargetShellCommand", () => { }); }); +describe("runAdapterExecutionTargetProcess", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + it("sanitizes inherited host env before SSH process execution", async () => { + vi.stubEnv("PATH", "/host/bin:/usr/bin"); + vi.stubEnv("HOME", "/Users/local"); + + const runChildProcessSpy = vi.spyOn(serverUtils, "runChildProcess").mockResolvedValue({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + }); + + await runAdapterExecutionTargetProcess( + "run-ssh-process", + { + kind: "remote", + transport: "ssh", + remoteCwd: "/srv/paperclip/workspace", + spec: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteCwd: "/srv/paperclip/workspace", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }, + "agent-cli", + ["--json"], + { + cwd: "/tmp/local", + env: { + PATH: "/host/bin:/usr/bin", + HOME: "/Users/local", + SAFE_VALUE: "visible", + }, + timeoutSec: 5, + graceSec: 1, + onLog: async () => {}, + }, + ); + + expect(runChildProcessSpy).toHaveBeenCalledWith( + "run-ssh-process", + "agent-cli", + ["--json"], + expect.objectContaining({ + env: { + SAFE_VALUE: "visible", + }, + }), + ); + }); +}); + describe("ensureAdapterExecutionTargetRuntimeCommandInstalled", () => { afterEach(() => { vi.restoreAllMocks(); diff --git a/packages/adapter-utils/src/execution-target.ts b/packages/adapter-utils/src/execution-target.ts index 563d1795e50..b51780d58fb 100644 --- a/packages/adapter-utils/src/execution-target.ts +++ b/packages/adapter-utils/src/execution-target.ts @@ -26,6 +26,7 @@ import { type RunProcessResult, type TerminalResultCleanupOptions, } from "./server-utils.js"; +import { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js"; import { preferredShellForSandbox } from "./sandbox-shell.js"; export interface AdapterLocalExecutionTarget { @@ -95,6 +96,8 @@ export interface AdapterExecutionTargetPaperclipBridgeHandle { stop(): Promise; } +export { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js"; + function parseObject(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -340,11 +343,12 @@ export async function runAdapterExecutionTargetProcess( ): Promise { if (target?.kind === "remote" && target.transport === "sandbox") { const runner = requireSandboxRunner(target); + const env = sanitizeRemoteExecutionEnv(options.env); return await runner.execute({ command, args, cwd: target.remoteCwd, - env: options.env, + env, stdin: options.stdin, timeoutMs: options.timeoutSec > 0 ? options.timeoutSec * 1000 : target.timeoutMs ?? undefined, onLog: options.onLog, @@ -354,9 +358,14 @@ export async function runAdapterExecutionTargetProcess( }); } + const env = + target?.kind === "remote" && target.transport === "ssh" + ? sanitizeRemoteExecutionEnv(options.env) + : options.env; + return await runChildProcess(runId, command, args, { cwd: options.cwd, - env: options.env, + env, stdin: options.stdin, timeoutSec: options.timeoutSec, graceSec: options.graceSec, @@ -376,9 +385,16 @@ export async function runAdapterExecutionTargetShellCommand( const onLog = options.onLog ?? (async () => {}); if (target?.kind === "remote") { const startedAt = new Date().toISOString(); + const env = sanitizeRemoteExecutionEnv(options.env); if (target.transport === "ssh") { try { - const result = await runSshCommand(target.spec, `sh -lc ${shellQuote(command)}`, { + // Pass the raw command — `runSshCommand` owns profile sourcing and + // the outer `sh -lc` wrapper. Wrapping again here would nest a second + // `sh -lc` after the explicit `env KEY=VAL` overrides, re-sourcing + // login profiles AFTER the override and silently undoing any + // identity var (NVM_DIR / PATH / etc.) that a profile re-exports. + const result = await runSshCommand(target.spec, command, { + env, timeoutMs: (options.timeoutSec ?? 15) * 1000, }); if (result.stdout) await onLog("stdout", result.stdout); @@ -435,7 +451,7 @@ export async function runAdapterExecutionTargetShellCommand( command: shellCommand, args: ["-lc", command], cwd: target.remoteCwd, - env: options.env, + env, timeoutMs: (options.timeoutSec ?? 15) * 1000, onLog, }); diff --git a/packages/adapter-utils/src/remote-execution-env.ts b/packages/adapter-utils/src/remote-execution-env.ts new file mode 100644 index 00000000000..9bc900228cc --- /dev/null +++ b/packages/adapter-utils/src/remote-execution-env.ts @@ -0,0 +1,49 @@ +const REMOTE_EXECUTION_ENV_IDENTITY_KEYS = new Set([ + "PATH", + "HOME", + "PWD", + "SHELL", + "USER", + "LOGNAME", + "NVM_DIR", + "TMPDIR", + "TMP", + "TEMP", + "XDG_CONFIG_HOME", + "XDG_CACHE_HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "XDG_RUNTIME_DIR", +]); + +function readEnvValueCaseInsensitive(env: NodeJS.ProcessEnv, key: string): string | undefined { + const direct = env[key]; + if (typeof direct === "string") return direct; + const upper = key.toUpperCase(); + for (const [candidateKey, candidateValue] of Object.entries(env)) { + if (candidateKey.toUpperCase() === upper && typeof candidateValue === "string") { + return candidateValue; + } + } + return undefined; +} + +export function sanitizeRemoteExecutionEnv( + env: Record, + inheritedEnv: NodeJS.ProcessEnv = process.env, +): Record { + const sanitized: Record = {}; + for (const [key, value] of Object.entries(env)) { + const normalizedKey = key.toUpperCase(); + if (!REMOTE_EXECUTION_ENV_IDENTITY_KEYS.has(normalizedKey)) { + sanitized[key] = value; + continue; + } + const inheritedValue = readEnvValueCaseInsensitive(inheritedEnv, key); + if (typeof inheritedValue === "string" && inheritedValue === value) { + continue; + } + sanitized[key] = value; + } + return sanitized; +} diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 1a07f643f8a..83420182b16 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { constants as fsConstants, promises as fs, type Dirent } from "node:fs"; import path from "node:path"; +import { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js"; import { buildSshSpawnTarget, type SshRemoteExecutionSpec } from "./ssh.js"; import { redactCommandText } from "./command-redaction.js"; import type { @@ -1039,54 +1040,11 @@ function quoteForCmd(arg: string) { return /[\s"&<>|^()]/.test(escaped) ? `"${escaped}"` : escaped; } -const SSH_REMOTE_ENV_IDENTITY_KEYS = new Set([ - "PATH", - "HOME", - "PWD", - "SHELL", - "USER", - "LOGNAME", - "NVM_DIR", - "TMPDIR", - "TMP", - "TEMP", - "XDG_CONFIG_HOME", - "XDG_CACHE_HOME", - "XDG_DATA_HOME", - "XDG_STATE_HOME", - "XDG_RUNTIME_DIR", -]); - -function readEnvValueCaseInsensitive(env: NodeJS.ProcessEnv, key: string): string | undefined { - const direct = env[key]; - if (typeof direct === "string") return direct; - const upper = key.toUpperCase(); - for (const [candidateKey, candidateValue] of Object.entries(env)) { - if (candidateKey.toUpperCase() === upper && typeof candidateValue === "string") { - return candidateValue; - } - } - return undefined; -} - export function sanitizeSshRemoteEnv( env: Record, inheritedEnv: NodeJS.ProcessEnv = process.env, ): Record { - const sanitized: Record = {}; - for (const [key, value] of Object.entries(env)) { - const normalizedKey = key.toUpperCase(); - if (!SSH_REMOTE_ENV_IDENTITY_KEYS.has(normalizedKey)) { - sanitized[key] = value; - continue; - } - const inheritedValue = readEnvValueCaseInsensitive(inheritedEnv, key); - if (typeof inheritedValue === "string" && inheritedValue === value) { - continue; - } - sanitized[key] = value; - } - return sanitized; + return sanitizeRemoteExecutionEnv(env, inheritedEnv); } function resolveWindowsCmdShell(env: NodeJS.ProcessEnv): string { @@ -1114,9 +1072,9 @@ async function resolveSpawnTarget( spec: remote, command, args, - env: sanitizeSshRemoteEnv(Object.fromEntries( + env: Object.fromEntries( Object.entries(options.remoteEnv ?? {}).filter((entry): entry is [string, string] => typeof entry[1] === "string"), - )), + ), }); return { command: sshResolved, diff --git a/packages/adapter-utils/src/ssh.ts b/packages/adapter-utils/src/ssh.ts index 39087b3e608..2b5dbbadb5d 100644 --- a/packages/adapter-utils/src/ssh.ts +++ b/packages/adapter-utils/src/ssh.ts @@ -721,6 +721,7 @@ export async function runSshCommand( config: SshConnectionConfig, remoteCommand: string, options: { + env?: Record; timeoutMs?: number; maxBuffer?: number; } = {}, @@ -730,12 +731,33 @@ export async function runSshCommand( const auth = await createSshAuthArgs(config); cleanup = auth.cleanup; const sshArgs = [...auth.args]; + const envEntries = Object.entries(options.env ?? {}) + .filter((entry): entry is [string, string] => typeof entry[1] === "string"); + for (const [key] of envEntries) { + if (!isValidShellEnvKey(key)) { + throw new Error(`Invalid SSH environment variable key: ${key}`); + } + } + + // Mirror buildSshSpawnTarget: source login profiles first, then run + // `env KEY=VAL cmd` so user-supplied identity overrides win over anything + // a profile re-exports. Without this, a remote profile that resets HOME + // / NVM_DIR / etc. would silently undo the explicit env passed in here. + const envArgs = envEntries.map(([key, value]) => `${key}=${shellQuote(value)}`); + const remoteScript = [ + 'if [ -f "$HOME/.profile" ]; then . "$HOME/.profile" >/dev/null 2>&1 || true; fi', + 'if [ -f "$HOME/.bash_profile" ]; then . "$HOME/.bash_profile" >/dev/null 2>&1 || true; fi', + 'if [ -f "$HOME/.zprofile" ]; then . "$HOME/.zprofile" >/dev/null 2>&1 || true; fi', + envArgs.length > 0 + ? `exec env ${envArgs.join(" ")} sh -c ${shellQuote(remoteCommand)}` + : `exec sh -c ${shellQuote(remoteCommand)}`, + ].join(" && "); sshArgs.push( "-p", String(config.port), `${config.username}@${config.host}`, - remoteCommand, + `sh -lc ${shellQuote(remoteScript)}`, ); return await execFileText("ssh", sshArgs, { diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index a4758084a75..e0959d5f72d 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -290,13 +290,6 @@ export async function testEnvironment( if (variant) args.push("--variant", variant); if (extraArgs.length > 0) args.push(...extraArgs); - // For remote targets, do NOT spread the host process.env into the - // probe env: it leaks macOS-only paths (HOME=/Users/..., host - // XDG_CONFIG_HOME, TMPDIR, etc.) into the remote shell, which causes - // opencode on the remote box to try to mkdir host paths like /Users. - // Match the pattern used by claude_local / codex_local / gemini_local - // probes: send only the user-configured adapter env across SSH. - const probeEnv = targetIsRemote ? preparedRuntimeConfig.env : runtimeEnv; try { const probe = await runAdapterExecutionTargetProcess( runId, @@ -305,7 +298,7 @@ export async function testEnvironment( args, { cwd, - env: probeEnv, + env: runtimeEnv, timeoutSec: 60, graceSec: 5, stdin: "Respond with hello.", diff --git a/packages/adapters/pi-local/src/server/test.ts b/packages/adapters/pi-local/src/server/test.ts index aef648ef549..782699ce7fa 100644 --- a/packages/adapters/pi-local/src/server/test.ts +++ b/packages/adapters/pi-local/src/server/test.ts @@ -259,17 +259,6 @@ export async function testEnvironment( args.push("--tools", "read"); if (extraArgs.length > 0) args.push(...extraArgs); - // For remote targets, do NOT spread the host process.env into the probe - // env: it leaks macOS-only PATH, HOME, TMPDIR, etc. into the remote shell. - // In particular the Mac PATH overrides the nvm-sourced PATH that - // buildSshSpawnTarget sets up, which on Linux SSH targets resolves `node` - // to /usr/bin/node (Node 18) instead of nvm's Node 22, causing pi-tui to - // crash with `Invalid regular expression flags` on its /v unicode regex. - // Match the pattern used by claude_local / codex_local / gemini_local / - // opencode_local probes: send only the user-configured adapter env across - // SSH. Local probes still get the full runtimeEnv. - const probeEnv = targetIsRemote ? normalizeEnv(env) : runtimeEnv; - try { const probe = await runAdapterExecutionTargetProcess( runId, @@ -278,7 +267,7 @@ export async function testEnvironment( args, { cwd, - env: probeEnv, + env: runtimeEnv, timeoutSec: 60, graceSec: 5, onLog: async () => {}, From 50db8c01d2885ddeb558ac55dde5cc0ed7957e76 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 5 May 2026 20:01:04 -0700 Subject: [PATCH 5/7] Serialize sandbox callback bridge against concurrent heartbeats (#5326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > **Stacked PR.** This PR's branch carries cumulative content from #5324 (bridge allowlist expand) and #5325 (env sanitization) — the mutex/sha256 logic in this PR sits on top of both. Reviewers should focus on the files this PR's commit touches: `packages/adapter-utils/src/sandbox-callback-bridge.{ts,test.ts}`, `packages/adapter-utils/src/ssh.ts`, and `packages/adapter-utils/src/ssh-fixture.test.ts`. Will rebase onto `master` and force-push once both prerequisite PRs are merged. ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Each agent that runs in a sandbox or via SSH talks back to the Paperclip server through a per-lease callback bridge whose entrypoint script is uploaded to the remote > - When two heartbeats target the same agent on the same machine concurrently, both upload the bridge entrypoint and both write to the same response files — producing torn-write races: `SyntaxError: Identifier 'randomUUID' has already been declared` from a concatenated upload, `mv: cannot stat …` from colliding `.json.tmp` writes, and 0-byte commits from a truncated stdin > - This pull request serializes those operations with a POSIX `mkdir`-mutex (PID liveness check + atomic rename) at the bridge entrypoint upload, applies the same lock to the bridge response writer, forwards stdin into remote ssh commands so the entrypoint payload arrives intact, and verifies a sha256 of the upload before promoting it > - The benefit is concurrent heartbeats no longer corrupt each other's bridge state ## What Changed - `packages/adapter-utils/src/sandbox-callback-bridge.ts`: serialize entrypoint upload and response writes via POSIX `mkdir`-mutex with PID liveness; sha256 the upload before promoting via `mv`; content-skip when the existing entrypoint already matches - `packages/adapter-utils/src/ssh.ts`: forward stdin into remote ssh commands through the SSH managed runtime so `cat > "$remote_upload"` actually receives the base64-encoded entrypoint - `packages/adapter-utils/src/ssh-fixture.test.ts`: cover the stdin-forwarded SSH path - `packages/adapter-utils/src/sandbox-callback-bridge.test.ts`: cover the mutex, content-skip, sha256-verify, and atomic-rename paths ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-utils` - `pnpm typecheck` clean - Manual: two parallel heartbeats targeting the same SSH agent no longer race on the bridge entrypoint or response files ## Risks Medium. Serializing previously-parallel operations adds latency on the contended path (one heartbeat waits on another), bounded by the entrypoint upload time. The mutex includes PID liveness so a crashed heartbeat doesn't deadlock subsequent ones. Sha256-verify gives a clear "torn upload" failure mode instead of silent 0-byte commits. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — tests cover mutex + sha256-verify + stdin-forwarded ssh - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- .../src/sandbox-callback-bridge.test.ts | 200 ++++++++++++ .../src/sandbox-callback-bridge.ts | 290 +++++++++++++++++- .../adapter-utils/src/ssh-fixture.test.ts | 36 +++ packages/adapter-utils/src/ssh.ts | 123 +++++++- 4 files changed, 633 insertions(+), 16 deletions(-) diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts index c18343afd66..cdc01fbe0d6 100644 --- a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts +++ b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts @@ -8,10 +8,12 @@ import { afterEach, describe, expect, it } from "vitest"; import { prepareCommandManagedRuntime } from "./command-managed-runtime.js"; import { authorizeSandboxCallbackBridgeRequestWithRoutes, + createCommandManagedSandboxCallbackBridgeQueueClient, createFileSystemSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, sandboxCallbackBridgeDirectories, + syncSandboxCallbackBridgeEntrypoint, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, } from "./sandbox-callback-bridge.js"; @@ -420,6 +422,98 @@ describe("sandbox callback bridge", () => { ); }); + it("serializes remote response writes so stop does not recreate a late orphaned response", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-response-lock-")); + cleanupDirs.push(rootDir); + + const localWorkspaceDir = path.join(rootDir, "local-workspace"); + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + await mkdir(localWorkspaceDir, { recursive: true }); + await mkdir(remoteWorkspaceDir, { recursive: true }); + await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge response lock test\n", "utf8"); + + const runner = createExecRunner(); + const bridgeAsset = await createSandboxCallbackBridgeAsset(); + cleanupFns.push(bridgeAsset.cleanup); + const prepared = await prepareCommandManagedRuntime({ + runner, + spec: { + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + }, + adapterKey: "codex", + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "bridge", localDir: bridgeAsset.localDir }], + }); + + const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge"); + const directories = sandboxCallbackBridgeDirectories(queueDir); + const bridgeToken = createSandboxCallbackBridgeToken(); + const seenRequestIds: string[] = []; + + const worker = await startSandboxCallbackBridgeWorker({ + client: createCommandManagedSandboxCallbackBridgeQueueClient({ + runner, + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + }), + queueDir, + authorizeRequest: async () => null, + handleRequest: async (request) => { + seenRequestIds.push(request.id); + await new Promise((resolve) => setTimeout(resolve, 100)); + return { + status: 200, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ok: true, id: request.id }), + }; + }, + }); + cleanupFns.push(async () => { + await worker.stop(); + }); + + const bridge = await startSandboxCallbackBridgeServer({ + runner, + remoteCwd: remoteWorkspaceDir, + assetRemoteDir: prepared.assetDirs.bridge, + queueDir, + bridgeToken, + timeoutMs: 30_000, + }); + cleanupFns.push(async () => { + await bridge.stop(); + }); + + const responsePromise = fetch(`${bridge.baseUrl}/api/agents/me`, { + headers: { + authorization: `Bearer ${bridgeToken}`, + }, + }); + + for (let attempt = 0; attempt < 50 && seenRequestIds.length === 0; attempt += 1) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + + expect(seenRequestIds).toHaveLength(1); + await worker.stop({ drainTimeoutMs: 10 }); + + const response = await responsePromise; + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Bridge worker stopped before request could be handled.", + }); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + await expect(readdir(directories.responsesDir)).resolves.toEqual([]); + await expect( + readdir(directories.responsesDir).then((entries) => + entries.filter((entry) => entry.endsWith(".tmp") || entry.includes(".paperclip-write.lock")), + ), + ).resolves.toEqual([]); + }); + it("rejects non-JSON request bodies and full queues at the bridge server", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-server-guards-")); cleanupDirs.push(rootDir); @@ -615,6 +709,112 @@ describe("sandbox callback bridge", () => { }); }); + it("reuses an already-uploaded bridge entrypoint when the remote file hash matches", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-sync-")); + cleanupDirs.push(rootDir); + + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const remoteAssetDir = path.posix.join( + remoteWorkspaceDir, + ".paperclip-runtime", + "codex", + "paperclip-bridge", + "server", + ); + await mkdir(remoteWorkspaceDir, { recursive: true }); + + const bridgeAsset = await createSandboxCallbackBridgeAsset(); + cleanupFns.push(bridgeAsset.cleanup); + const originalSource = await readFile(bridgeAsset.entrypoint, "utf8"); + const expandedSource = `${originalSource}\n// bridge payload padding\n`; + await writeFile(bridgeAsset.entrypoint, expandedSource, "utf8"); + + const runner = createExecRunner(); + + const first = await syncSandboxCallbackBridgeEntrypoint({ + runner, + remoteCwd: remoteWorkspaceDir, + assetRemoteDir: remoteAssetDir, + bridgeAsset, + timeoutMs: 30_000, + }); + const second = await syncSandboxCallbackBridgeEntrypoint({ + runner, + remoteCwd: remoteWorkspaceDir, + assetRemoteDir: remoteAssetDir, + bridgeAsset, + timeoutMs: 30_000, + }); + + expect(first.uploaded).toBe(true); + expect(second.uploaded).toBe(false); + await expect(readFile(path.posix.join(remoteAssetDir, "paperclip-bridge-server.mjs"), "utf8")).resolves.toBe(expandedSource); + await expect( + readdir(remoteAssetDir).then((entries) => + entries.filter( + (entry) => + entry.endsWith(".paperclip-upload.b64") || + entry.endsWith(".partial") || + entry === ".paperclip-bridge-upload.lock", + ), + ), + ).resolves.toEqual([]); + }); + + it("rejects a corrupted bridge entrypoint upload without committing a torn remote file", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-sync-corrupt-")); + cleanupDirs.push(rootDir); + + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const remoteAssetDir = path.posix.join( + remoteWorkspaceDir, + ".paperclip-runtime", + "codex", + "paperclip-bridge", + "server", + ); + await mkdir(remoteWorkspaceDir, { recursive: true }); + + const bridgeAsset = await createSandboxCallbackBridgeAsset(); + cleanupFns.push(bridgeAsset.cleanup); + const runner = { + execute: async (input: { + command: string; + args?: string[]; + cwd?: string; + env?: Record; + stdin?: string; + timeoutMs?: number; + }) => + await createExecRunner().execute({ + ...input, + stdin: input.stdin != null ? "" : input.stdin, + }), + }; + + await expect( + syncSandboxCallbackBridgeEntrypoint({ + runner, + remoteCwd: remoteWorkspaceDir, + assetRemoteDir: remoteAssetDir, + bridgeAsset, + timeoutMs: 30_000, + }), + ).rejects.toThrow(/sha mismatch/i); + + await expect(readFile(path.posix.join(remoteAssetDir, "paperclip-bridge-server.mjs"), "utf8")).rejects.toThrow(); + await expect( + readdir(remoteAssetDir).then((entries) => + entries.filter( + (entry) => + entry.endsWith(".paperclip-upload.b64") || + entry.endsWith(".partial") || + entry === ".paperclip-bridge-upload.lock", + ), + ), + ).resolves.toEqual([]); + }); + it("permits the documented heartbeat surface and denies unrelated routes", () => { const allowed: Array<{ method: string; path: string }> = [ { method: "GET", path: "/api/agents/me" }, diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.ts b/packages/adapter-utils/src/sandbox-callback-bridge.ts index cb020046c13..d545616aa59 100644 --- a/packages/adapter-utils/src/sandbox-callback-bridge.ts +++ b/packages/adapter-utils/src/sandbox-callback-bridge.ts @@ -1,4 +1,4 @@ -import { randomBytes, randomUUID } from "node:crypto"; +import { createHash, randomBytes, randomUUID } from "node:crypto"; import { promises as fs } from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -145,6 +145,13 @@ export interface SandboxCallbackBridgeQueueClient { listJsonFiles(remotePath: string): Promise; readTextFile(remotePath: string): Promise; writeTextFile(remotePath: string, body: string): Promise; + writeResponseFile?( + responsePath: string, + body: string, + options?: { + requestPath?: string | null; + }, + ): Promise<{ wrote: boolean }>; rename(fromPath: string, toPath: string): Promise; remove(remotePath: string): Promise; } @@ -196,12 +203,14 @@ async function runShell( script: string, timeoutMs: number, shellCommand: "bash" | "sh" = "sh", + stdin?: string, ): Promise { return await runner.execute({ command: shellCommand, args: ["-lc", script], cwd, timeoutMs, + stdin, }); } @@ -218,6 +227,43 @@ function base64Chunks(body: string): string[] { return out; } +async function pathExists(filePath: string): Promise { + return await fs.stat(filePath).then(() => true).catch(() => false); +} + +function buildRemotePidLockAcquireScript(lockDirExpr: string, timeoutMessage: string): string[] { + return [ + "attempts=0", + `while ! mkdir ${lockDirExpr} 2>/dev/null; do`, + " holder_pid=\"\"", + ` if [ -s ${lockDirExpr}/pid ]; then`, + ` holder_pid="$(cat ${lockDirExpr}/pid 2>/dev/null || true)"`, + " fi", + " if [ -n \"$holder_pid\" ] && ! kill -0 \"$holder_pid\" 2>/dev/null; then", + ` rm -rf ${lockDirExpr}`, + " continue", + " fi", + " attempts=$((attempts + 1))", + " if [ \"$attempts\" -ge 600 ]; then", + ` echo ${shellQuote(timeoutMessage)} >&2`, + " exit 1", + " fi", + " sleep 0.05", + "done", + `printf '%s\\n' "$$" > ${lockDirExpr}/pid`, + ]; +} + +function buildRemotePidLockCleanupScript(lockDirExpr: string, cleanupLines: string[]): string[] { + return [ + "cleanup() {", + ...cleanupLines.map((line) => ` ${line}`), + ` rm -rf ${lockDirExpr}`, + "}", + "trap cleanup EXIT INT TERM", + ]; +} + export function createSandboxCallbackBridgeToken(bytes = DEFAULT_BRIDGE_TOKEN_BYTES): string { return randomBytes(bytes).toString("base64url"); } @@ -315,6 +361,80 @@ export function createFileSystemSandboxCallbackBridgeQueueClient(): SandboxCallb await fs.mkdir(path.posix.dirname(remotePath), { recursive: true }); await fs.writeFile(remotePath, body, "utf8"); }, + writeResponseFile: async (responsePath, body, options = {}) => { + const responseDir = path.posix.dirname(responsePath); + const tempPath = `${responsePath}.tmp`; + const lockDir = `${responsePath}.paperclip-write.lock`; + const lockPidFile = `${lockDir}/pid`; + if (options.requestPath) { + const requestExists = await pathExists(options.requestPath); + if (!requestExists) { + return { wrote: false }; + } + } + await fs.mkdir(responseDir, { recursive: true }); + // PID-liveness mkdir-mutex: mirrors the shell-based bridge mutex so a + // crashed holder (SIGKILL / OOM) doesn't deadlock subsequent writers + // for the full timeout window. + let attempts = 0; + while (true) { + try { + await fs.mkdir(lockDir); + await fs.writeFile(lockPidFile, `${process.pid}\n`, "utf8"); + break; + } catch (error) { + const code = (error as NodeJS.ErrnoException)?.code; + if (code !== "EEXIST") { + throw error; + } + let holderPid: number | null = null; + try { + const raw = await fs.readFile(lockPidFile, "utf8"); + const parsed = Number.parseInt(raw.trim(), 10); + if (Number.isFinite(parsed) && parsed > 0) holderPid = parsed; + } catch { + // pid file missing or unreadable — treat as stale lock + } + let holderAlive = false; + if (holderPid !== null) { + try { + process.kill(holderPid, 0); + holderAlive = true; + } catch { + holderAlive = false; + } + } + if (!holderAlive) { + await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined); + continue; + } + attempts += 1; + if (attempts >= 600) { + throw new Error("Timed out acquiring sandbox callback bridge response lock."); + } + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } + + try { + if (options.requestPath) { + const requestExists = await pathExists(options.requestPath); + if (!requestExists) { + return { wrote: false }; + } + } + const responseExists = await pathExists(responsePath); + if (responseExists) { + return { wrote: false }; + } + await fs.writeFile(tempPath, body, "utf8"); + await fs.rename(tempPath, responsePath); + return { wrote: true }; + } finally { + await fs.rm(tempPath, { force: true }).catch(() => undefined); + await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined); + } + }, rename: async (fromPath, toPath) => { await fs.mkdir(path.posix.dirname(toPath), { recursive: true }); await fs.rename(fromPath, toPath); @@ -385,6 +505,53 @@ export function createCommandManagedSandboxCallbackBridgeQueueClient(input: { `base64 -d < ${shellQuote(tempPath)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(tempPath)}`, ); }, + writeResponseFile: async (responsePath, body, options = {}) => { + const responseDir = path.posix.dirname(responsePath); + const tempPath = `${responsePath}.tmp`; + const lockDir = `${responsePath}.paperclip-write.lock`; + const requestPath = options.requestPath?.trim() || ""; + const result = await runShell( + input.runner, + input.remoteCwd, + [ + "set -eu", + `response_dir=${shellQuote(responseDir)}`, + `response_path=${shellQuote(responsePath)}`, + `temp_path=${shellQuote(tempPath)}`, + `lock_dir=${shellQuote(lockDir)}`, + `request_path=${shellQuote(requestPath)}`, + "mkdir -p \"$response_dir\"", + ...buildRemotePidLockAcquireScript("\"$lock_dir\"", "Timed out acquiring sandbox callback bridge response lock."), + ...buildRemotePidLockCleanupScript("\"$lock_dir\"", [ + "rm -f \"$temp_path\"", + ]), + "if [ -n \"$request_path\" ] && [ ! -f \"$request_path\" ]; then", + " printf '{\"wrote\":false}\\n'", + " exit 0", + "fi", + "if [ -f \"$response_path\" ]; then", + " printf '{\"wrote\":false}\\n'", + " exit 0", + "fi", + "cat > \"$temp_path\"", + "mv \"$temp_path\" \"$response_path\"", + "printf '{\"wrote\":true}\\n'", + ].join("\n"), + timeoutMs, + shellCommand, + body, + ); + requireSuccessfulResult(`write bridge response ${responsePath}`, result); + try { + return { + wrote: JSON.parse(result.stdout.trim())?.wrote === true, + }; + } catch (error) { + throw new Error( + `Sandbox callback bridge response write wrote invalid result JSON: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, rename: async (fromPath, toPath) => { await runChecked( `rename ${fromPath}`, @@ -399,11 +566,17 @@ export function createCommandManagedSandboxCallbackBridgeQueueClient(input: { async function writeBridgeResponse( client: SandboxCallbackBridgeQueueClient, + requestPath: string, responsePath: string, response: SandboxCallbackBridgeResponse, ) { + const body = `${JSON.stringify(response)}\n`; + if (client.writeResponseFile) { + await client.writeResponseFile(responsePath, body, { requestPath }); + return; + } const tempPath = `${responsePath}.tmp`; - await client.writeTextFile(tempPath, `${JSON.stringify(response)}\n`); + await client.writeTextFile(tempPath, body); await client.rename(tempPath, responsePath); } @@ -447,7 +620,7 @@ export async function startSandboxCallbackBridgeWorker(input: { request = JSON.parse(raw) as SandboxCallbackBridgeRequest; } catch { const requestId = fileName.replace(/\.json$/i, "") || randomUUID(); - await writeBridgeResponse(input.client, responsePath, { + await writeBridgeResponse(input.client, requestPath, responsePath, { id: requestId, status: 400, headers: { "content-type": "application/json" }, @@ -460,7 +633,7 @@ export async function startSandboxCallbackBridgeWorker(input: { const denialReason = await authorizeRequest(request); if (denialReason) { - await writeBridgeResponse(input.client, responsePath, { + await writeBridgeResponse(input.client, requestPath, responsePath, { id: request.id, status: 403, headers: { "content-type": "application/json" }, @@ -477,7 +650,7 @@ export async function startSandboxCallbackBridgeWorker(input: { if (Buffer.byteLength(responseBody, "utf8") > maxBodyBytes) { throw new Error(`Bridge response body exceeded the configured size limit of ${maxBodyBytes} bytes.`); } - await writeBridgeResponse(input.client, responsePath, { + await writeBridgeResponse(input.client, requestPath, responsePath, { id: request.id, status: result.status, headers: result.headers ?? {}, @@ -488,7 +661,7 @@ export async function startSandboxCallbackBridgeWorker(input: { console.warn( `[paperclip] sandbox callback bridge handler failed for ${request.id}: ${error instanceof Error ? error.message : String(error)}`, ); - await writeBridgeResponse(input.client, responsePath, { + await writeBridgeResponse(input.client, requestPath, responsePath, { id: request.id, status: 502, headers: { "content-type": "application/json" }, @@ -511,7 +684,7 @@ export async function startSandboxCallbackBridgeWorker(input: { try { const raw = await input.client.readTextFile(requestPath); const parsed = JSON.parse(raw) as Partial; - await writeBridgeResponse(input.client, responsePath, { + await writeBridgeResponse(input.client, requestPath, responsePath, { id: typeof parsed.id === "string" && parsed.id.length > 0 ? parsed.id : requestId, status: 503, headers: { "content-type": "application/json" }, @@ -578,6 +751,99 @@ export async function startSandboxCallbackBridgeWorker(input: { }; } +export async function syncSandboxCallbackBridgeEntrypoint(input: { + runner: CommandManagedRuntimeRunner; + remoteCwd: string; + assetRemoteDir: string; + bridgeAsset: SandboxCallbackBridgeAsset; + timeoutMs?: number | null; + shellCommand?: "bash" | "sh" | null; +}): Promise<{ remoteEntrypoint: string; sha256: string; uploaded: boolean }> { + const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS); + const shellCommand = preferredShellForSandbox(input.shellCommand); + const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT); + const remoteEntrypointPartial = `${remoteEntrypoint}.partial`; + const remoteUploadPath = `${remoteEntrypoint}.paperclip-upload.b64`; + const remoteLockDir = path.posix.join(input.assetRemoteDir, ".paperclip-bridge-upload.lock"); + const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8"); + const entrypointBase64 = toBuffer(Buffer.from(entrypointSource, "utf8")).toString("base64"); + const sha256 = createHash("sha256").update(entrypointSource, "utf8").digest("hex"); + + const syncResult = await runShell( + input.runner, + input.remoteCwd, + [ + "set -eu", + `remote_dir=${shellQuote(input.assetRemoteDir)}`, + `remote_path=${shellQuote(remoteEntrypoint)}`, + `remote_partial=${shellQuote(remoteEntrypointPartial)}`, + `remote_upload=${shellQuote(remoteUploadPath)}`, + `lock_dir=${shellQuote(remoteLockDir)}`, + `expected_sha=${shellQuote(sha256)}`, + "hash_file() {", + " if command -v sha256sum >/dev/null 2>&1; then", + " sha256sum \"$1\" | awk '{print $1}'", + " return 0", + " fi", + " if command -v shasum >/dev/null 2>&1; then", + " shasum -a 256 \"$1\" | awk '{print $1}'", + " return 0", + " fi", + " return 127", + "}", + "mkdir -p \"$remote_dir\"", + ...buildRemotePidLockAcquireScript("\"$lock_dir\"", "Timed out acquiring sandbox callback bridge upload lock."), + ...buildRemotePidLockCleanupScript("\"$lock_dir\"", [ + "rm -f \"$remote_upload\" \"$remote_partial\"", + ]), + "current_sha=\"\"", + "if [ -f \"$remote_path\" ]; then", + " current_sha=\"$(hash_file \"$remote_path\" 2>/dev/null)\" || current_sha=\"\"", + "fi", + "if [ -n \"$current_sha\" ] && [ \"$current_sha\" = \"$expected_sha\" ]; then", + " printf '{\"uploaded\":false}\\n'", + " exit 0", + "fi", + "rm -f \"$remote_upload\" \"$remote_partial\"", + "cat > \"$remote_upload\"", + "base64 -d < \"$remote_upload\" > \"$remote_partial\"", + // Verify upload integrity. If neither sha256sum nor shasum is on PATH + // (minimal Alpine/scratch images), surface the missing-tool error + // instead of a misleading "sha mismatch" — the verify step is then + // best-effort and we trust base64-decode + atomic rename below. + "if partial_sha=\"$(hash_file \"$remote_partial\" 2>/dev/null)\"; then", + " if [ \"$partial_sha\" != \"$expected_sha\" ]; then", + " echo \"Sandbox callback bridge entrypoint upload sha mismatch.\" >&2", + " exit 1", + " fi", + "else", + " echo \"Sandbox callback bridge entrypoint sha verify skipped: no sha256sum/shasum on remote.\" >&2", + "fi", + "mv \"$remote_partial\" \"$remote_path\"", + "printf '{\"uploaded\":true}\\n'", + ].join("\n"), + timeoutMs, + shellCommand, + entrypointBase64, + ); + requireSuccessfulResult("sync sandbox callback bridge entrypoint", syncResult); + + let uploaded = false; + try { + uploaded = JSON.parse(syncResult.stdout.trim())?.uploaded === true; + } catch (error) { + throw new Error( + `Sandbox callback bridge sync wrote invalid result JSON: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + return { + remoteEntrypoint, + sha256, + uploaded, + }; +} + export async function startSandboxCallbackBridgeServer(input: { runner: CommandManagedRuntimeRunner; remoteCwd: string; @@ -598,17 +864,17 @@ export async function startSandboxCallbackBridgeServer(input: { const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS); const shellCommand = preferredShellForSandbox(input.shellCommand); const directories = sandboxCallbackBridgeDirectories(input.queueDir); - const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT); + let remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT); if (input.bridgeAsset) { - const assetClient = createCommandManagedSandboxCallbackBridgeQueueClient({ + const assetSync = await syncSandboxCallbackBridgeEntrypoint({ runner: input.runner, remoteCwd: input.remoteCwd, + assetRemoteDir: input.assetRemoteDir, + bridgeAsset: input.bridgeAsset, timeoutMs, shellCommand, }); - await assetClient.makeDir(input.assetRemoteDir); - const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8"); - await assetClient.writeTextFile(remoteEntrypoint, entrypointSource); + remoteEntrypoint = assetSync.remoteEntrypoint; } const env = buildSandboxCallbackBridgeEnv({ queueDir: input.queueDir, diff --git a/packages/adapter-utils/src/ssh-fixture.test.ts b/packages/adapter-utils/src/ssh-fixture.test.ts index dd1ae28ae40..7ca4a657a9b 100644 --- a/packages/adapter-utils/src/ssh-fixture.test.ts +++ b/packages/adapter-utils/src/ssh-fixture.test.ts @@ -70,6 +70,42 @@ describe("ssh env-lab fixture", () => { expect(stopped.running).toBe(false); }); + it("forwards stdin to remote SSH commands", async () => { + const support = await getSshEnvLabSupport(); + if (!support.supported) { + console.warn( + `Skipping SSH stdin forwarding test: ${support.reason ?? "unsupported environment"}`, + ); + return; + } + + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ssh-fixture-")); + cleanupDirs.push(rootDir); + const statePath = path.join(rootDir, "state.json"); + + const started = await startSshEnvLabFixture({ statePath }); + const config = await buildSshEnvLabFixtureConfig(started); + const remotePath = path.posix.join(started.workspaceDir, "stdin-forwarded.txt"); + + await runSshCommand( + config, + `sh -lc 'cat > ${JSON.stringify(remotePath)}'`, + { + stdin: "hello over ssh stdin\n", + timeoutMs: 30_000, + maxBuffer: 256 * 1024, + }, + ); + + const result = await runSshCommand( + config, + `sh -lc 'cat ${JSON.stringify(remotePath)}'`, + { timeoutMs: 30_000, maxBuffer: 256 * 1024 }, + ); + + expect(result.stdout).toBe("hello over ssh stdin\n"); + }); + it("does not treat an unrelated reused pid as the running fixture", async () => { const support = await getSshEnvLabSupport(); if (!support.supported) { diff --git a/packages/adapter-utils/src/ssh.ts b/packages/adapter-utils/src/ssh.ts index 2b5dbbadb5d..5070461c664 100644 --- a/packages/adapter-utils/src/ssh.ts +++ b/packages/adapter-utils/src/ssh.ts @@ -61,6 +61,7 @@ export function createSshCommandManagedRuntimeRunner(input: { try { const result = await runSshCommand(input.spec, remoteCommand, { + stdin: commandInput.stdin, timeoutMs: commandInput.timeoutMs, maxBuffer: maxBufferBytes, }); @@ -205,6 +206,113 @@ async function execFileText( }); } +async function spawnText( + file: string, + args: string[], + options: { + stdin?: string; + timeout?: number; + maxBuffer?: number; + } = {}, +): Promise { + return await new Promise((resolve, reject) => { + const child = spawn(file, args, { + stdio: [options.stdin != null ? "pipe" : "ignore", "pipe", "pipe"], + }); + + const maxBuffer = options.maxBuffer ?? 1024 * 128; + let stdout = ""; + let stderr = ""; + let settled = false; + let timedOut = false; + + const finishReject = (error: Error & { stdout?: string; stderr?: string; code?: number | null; killed?: boolean }) => { + if (settled) return; + settled = true; + error.stdout = stdout; + error.stderr = stderr; + error.killed = timedOut; + reject(error); + }; + + const append = ( + streamName: "stdout" | "stderr", + chunk: unknown, + ) => { + const text = String(chunk); + if (streamName === "stdout") { + stdout += text; + } else { + stderr += text; + } + if (Buffer.byteLength(stdout, "utf8") > maxBuffer || Buffer.byteLength(stderr, "utf8") > maxBuffer) { + child.kill("SIGTERM"); + finishReject(Object.assign(new Error(`Process output exceeded maxBuffer of ${maxBuffer} bytes.`), { + code: null, + })); + } + }; + + let killEscalation: NodeJS.Timeout | null = null; + const timeout = options.timeout && options.timeout > 0 + ? setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + // Escalate to SIGKILL after a 5s grace window so a hung remote + // command that ignores SIGTERM cannot keep the child alive + // indefinitely. + killEscalation = setTimeout(() => { + try { + child.kill("SIGKILL"); + } catch { + // child may have already exited between the SIGTERM and the + // escalation — that's fine. + } + }, 5_000); + killEscalation.unref?.(); + }, options.timeout) + : null; + + const clearTimers = () => { + if (timeout) clearTimeout(timeout); + if (killEscalation) clearTimeout(killEscalation); + }; + + child.stdout?.on("data", (chunk) => { + append("stdout", chunk); + }); + child.stderr?.on("data", (chunk) => { + append("stderr", chunk); + }); + + child.on("error", (error) => { + clearTimers(); + finishReject(Object.assign(error, { code: null })); + }); + + child.on("close", (code, signal) => { + clearTimers(); + if (settled) return; + settled = true; + if (code === 0) { + resolve({ stdout, stderr }); + return; + } + reject(Object.assign(new Error(stderr.trim() || stdout.trim() || `Process exited with code ${code ?? -1}`), { + stdout, + stderr, + code, + signal, + killed: timedOut, + })); + }); + + if (options.stdin != null && child.stdin) { + child.stdin.end(options.stdin); + } + }); +} + async function runLocalGit( localDir: string, args: string[], @@ -722,6 +830,7 @@ export async function runSshCommand( remoteCommand: string, options: { env?: Record; + stdin?: string; timeoutMs?: number; maxBuffer?: number; } = {}, @@ -760,10 +869,16 @@ export async function runSshCommand( `sh -lc ${shellQuote(remoteScript)}`, ); - return await execFileText("ssh", sshArgs, { - timeout: options.timeoutMs ?? 15_000, - maxBuffer: options.maxBuffer ?? 1024 * 128, - }); + return options.stdin != null + ? await spawnText("ssh", sshArgs, { + stdin: options.stdin, + timeout: options.timeoutMs ?? 15_000, + maxBuffer: options.maxBuffer ?? 1024 * 128, + }) + : await execFileText("ssh", sshArgs, { + timeout: options.timeoutMs ?? 15_000, + maxBuffer: options.maxBuffer ?? 1024 * 128, + }); } finally { await cleanup(); } From 454edfe81ee8f255577132c79b6b77e2ff58a16a Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 6 May 2026 06:05:58 -0500 Subject: [PATCH 6/7] Add recovery handoff system notices (#5289) ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Agent runs can end productively while the source issue still lacks a durable final disposition. > - That leaves the control plane unsure whether to resume, escalate, or close the work. > - Issue comments also need a presentation contract so system-authored recovery notices can render as first-class thread messages without overloading normal comments. > - This pull request adds successful-run handoff recovery, comment presentation metadata, and system notice rendering. > - The benefit is stricter task liveness with clearer operator-facing recovery state. ## What Changed - Added successful-run handoff decisions, wake payloads, escalation behavior, and recovery tests. - Added issue comment presentation metadata with migration `0078_white_darwin.sql` and shared/server/company portability support. - Rendered recovery/system notices in issue chat with dedicated UI components, fixtures, tests, and storybook/lab coverage. - Included the current recovery model-profile hint patch so automatic recovery follow-ups use the cheap profile. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/services/recovery/successful-run-handoff.test.ts ui/src/components/SystemNotice.test.tsx ui/src/lib/system-notice-comment.test.ts ui/src/components/IssueChatThreadSystemNotice.test.tsx` ## Risks - Migration-bearing PR: merge this before any other branch that might later add a migration. - The branch touches both recovery services and issue-thread rendering, so review should pay attention to recovery wake idempotency and comment metadata compatibility. ## Model Used - OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with shell/git/GitHub CLI tool use. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- cli/src/__tests__/company.test.ts | 2 + .../db/src/migrations/0078_white_darwin.sql | 3 + .../db/src/migrations/meta/0078_snapshot.json | 16373 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/issue_comments.ts | 6 +- packages/plugins/sdk/src/testing.ts | 3 + packages/shared/src/constants.ts | 19 + packages/shared/src/index.ts | 26 + .../shared/src/types/company-portability.ts | 13 + packages/shared/src/types/index.ts | 13 + packages/shared/src/types/issue.ts | 86 + .../src/validators/company-portability.ts | 16 + packages/shared/src/validators/index.ts | 5 + packages/shared/src/validators/issue.test.ts | 40 + packages/shared/src/validators/issue.ts | 91 + .../src/__tests__/company-portability.test.ts | 186 + ...artbeat-active-run-output-watchdog.test.ts | 1 + .../heartbeat-comment-wake-batching.test.ts | 66 +- .../heartbeat-dependency-scheduling.test.ts | 66 +- ...eartbeat-issue-liveness-escalation.test.ts | 2 + .../heartbeat-process-recovery.test.ts | 473 +- .../heartbeat-retry-scheduling.test.ts | 1 + .../issue-comment-reopen-routes.test.ts | 75 + .../__tests__/issue-monitor-scheduler.test.ts | 2 + .../issues-goal-context-routes.test.ts | 15 +- .../productivity-review-service.test.ts | 1 + .../src/__tests__/run-continuations.test.ts | 2 + .../server-startup-feedback-export.test.ts | 1 + server/src/index.ts | 2 + server/src/routes/issues.ts | 119 +- server/src/services/company-portability.ts | 141 +- server/src/services/feedback.ts | 21 + server/src/services/heartbeat.ts | 382 +- server/src/services/issues.ts | 62 +- server/src/services/productivity-review.ts | 13 +- server/src/services/recovery/index.ts | 20 + .../services/recovery/model-profile-hint.ts | 14 + .../recovery/run-liveness-continuations.ts | 9 +- server/src/services/recovery/service.ts | 225 +- .../recovery/successful-run-handoff.test.ts | 295 + .../recovery/successful-run-handoff.ts | 405 + ui/src/components/CommentThread.test.tsx | 6 + ui/src/components/IssueBlockedNotice.test.tsx | 63 + ui/src/components/IssueBlockedNotice.tsx | 112 +- ui/src/components/IssueChatThread.test.tsx | 21 + ui/src/components/IssueChatThread.tsx | 187 + .../IssueChatThreadSystemNotice.test.tsx | 398 + ui/src/components/IssuesList.tsx | 11 + ui/src/components/KanbanBoard.tsx | 12 + ui/src/components/SystemNotice.test.tsx | 197 + ui/src/components/SystemNotice.tsx | 337 + ui/src/fixtures/issueChatLongThreadFixture.ts | 3 + ui/src/fixtures/issueChatUxFixtures.ts | 6 +- .../issueThreadInteractionFixtures.ts | 3 + ui/src/fixtures/systemNoticeFixtures.ts | 204 + ui/src/lib/activity-format.test.ts | 9 + ui/src/lib/activity-format.ts | 6 + ui/src/lib/issue-chat-messages.test.ts | 4 + ui/src/lib/issue-chat-messages.ts | 17 +- ui/src/lib/optimistic-issue-comments.test.ts | 54 + ui/src/lib/optimistic-issue-comments.ts | 3 + ui/src/lib/successful-run-handoff.test.ts | 37 + ui/src/lib/successful-run-handoff.ts | 90 + ui/src/lib/system-notice-comment.test.ts | 143 + ui/src/lib/system-notice-comment.ts | 125 + ui/src/pages/IssueDetail.tsx | 37 +- ui/src/pages/SystemNoticeUxLab.tsx | 403 + .../stories/chat-comments.stories.tsx | 40 + .../successful-run-handoff.stories.tsx | 196 + ui/storybook/stories/ux-labs.stories.tsx | 18 + 70 files changed, 21919 insertions(+), 125 deletions(-) create mode 100644 packages/db/src/migrations/0078_white_darwin.sql create mode 100644 packages/db/src/migrations/meta/0078_snapshot.json create mode 100644 server/src/services/recovery/model-profile-hint.ts create mode 100644 server/src/services/recovery/successful-run-handoff.test.ts create mode 100644 server/src/services/recovery/successful-run-handoff.ts create mode 100644 ui/src/components/IssueBlockedNotice.test.tsx create mode 100644 ui/src/components/IssueChatThreadSystemNotice.test.tsx create mode 100644 ui/src/components/SystemNotice.test.tsx create mode 100644 ui/src/components/SystemNotice.tsx create mode 100644 ui/src/fixtures/systemNoticeFixtures.ts create mode 100644 ui/src/lib/successful-run-handoff.test.ts create mode 100644 ui/src/lib/successful-run-handoff.ts create mode 100644 ui/src/lib/system-notice-comment.test.ts create mode 100644 ui/src/lib/system-notice-comment.ts create mode 100644 ui/src/pages/SystemNoticeUxLab.tsx create mode 100644 ui/storybook/stories/successful-run-handoff.stories.tsx diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 2e4fcdece28..144b3147dcb 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -244,6 +244,7 @@ describe("renderCompanyImportPreview", () => { billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, + comments: [], metadata: null, }, ], @@ -460,6 +461,7 @@ describe("import selection catalog", () => { billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, + comments: [], metadata: null, }, ], diff --git a/packages/db/src/migrations/0078_white_darwin.sql b/packages/db/src/migrations/0078_white_darwin.sql new file mode 100644 index 00000000000..34c67985d70 --- /dev/null +++ b/packages/db/src/migrations/0078_white_darwin.sql @@ -0,0 +1,3 @@ +ALTER TABLE "issue_comments" ADD COLUMN "author_type" text;--> statement-breakpoint +ALTER TABLE "issue_comments" ADD COLUMN "presentation" jsonb;--> statement-breakpoint +ALTER TABLE "issue_comments" ADD COLUMN "metadata" jsonb; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0078_snapshot.json b/packages/db/src/migrations/meta/0078_snapshot.json new file mode 100644 index 00000000000..2298a0a3dee --- /dev/null +++ b/packages/db/src/migrations/meta/0078_snapshot.json @@ -0,0 +1,16373 @@ +{ + "id": "50cf2dfe-df7b-4f02-a169-edbae599cf39", + "prevId": "c0bac1d6-c931-4bef-b3d6-f7746ab492ac", + "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" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_default_environment_idx": { + "name": "agents_company_default_environment_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "default_environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attachment_max_bytes": { + "name": "attachment_max_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10485760 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_leases": { + "name": "environment_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "lease_policy": { + "name": "lease_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ephemeral'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_status": { + "name": "cleanup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_leases_company_environment_status_idx": { + "name": "environment_leases_company_environment_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_execution_workspace_idx": { + "name": "environment_leases_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_issue_idx": { + "name": "environment_leases_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_last_used_idx": { + "name": "environment_leases_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_company_status_idx": { + "name": "environments_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": {} + }, + "environments_company_driver_idx": { + "name": "environments_company_driver_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_company_name_idx": { + "name": "environments_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": { + "environments_company_id_companies_id_fk": { + "name": "environments_company_id_companies_id_fk", + "tableFrom": "environments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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_run_watchdog_decisions": { + "name": "heartbeat_run_watchdog_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_issue_id": { + "name": "evaluation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_watchdog_decisions_company_run_created_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_watchdog_decisions_company_run_snooze_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_snooze_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_watchdog_decisions_company_id_companies_id_fk": { + "name": "heartbeat_run_watchdog_decisions_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk": { + "name": "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "issues", + "columnsFrom": [ + "evaluation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_at": { + "name": "last_output_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_seq": { + "name": "last_output_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_output_stream": { + "name": "last_output_stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_output_bytes": { + "name": "last_output_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_at": { + "name": "scheduled_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_retry_attempt": { + "name": "scheduled_retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_reason": { + "name": "scheduled_retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "liveness_state": { + "name": "liveness_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "liveness_reason": { + "name": "liveness_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation_attempt": { + "name": "continuation_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_useful_action_at": { + "name": "last_useful_action_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_liveness_idx": { + "name": "heartbeat_runs_company_liveness_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "liveness_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_last_output_idx": { + "name": "heartbeat_runs_company_status_last_output_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_output_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_process_started_idx": { + "name": "heartbeat_runs_company_status_process_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "presentation": { + "name": "presentation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_reference_mentions": { + "name": "issue_reference_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text": { + "name": "matched_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_reference_mentions_company_source_issue_idx": { + "name": "issue_reference_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_target_issue_idx": { + "name": "issue_reference_mentions_company_target_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_issue_pair_idx": { + "name": "issue_reference_mentions_company_issue_pair_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_record_uq": { + "name": "issue_reference_mentions_company_source_mention_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_null_record_uq": { + "name": "issue_reference_mentions_company_source_mention_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_reference_mentions_source_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_source_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_thread_interactions": { + "name": "issue_thread_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'wake_assignee'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_thread_interactions_issue_idx": { + "name": "issue_thread_interactions_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_created_at_idx": { + "name": "issue_thread_interactions_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_status_idx": { + "name": "issue_thread_interactions_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_idempotency_uq": { + "name": "issue_thread_interactions_company_issue_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_source_comment_idx": { + "name": "issue_thread_interactions_source_comment_idx", + "columns": [ + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_comment_id_issue_comments_id_fk": { + "name": "issue_thread_interactions_source_comment_id_issue_comments_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issue_comments", + "columnsFrom": [ + "source_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_created_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_resolved_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_hold_members": { + "name": "issue_tree_hold_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_identifier": { + "name": "issue_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_status": { + "name": "issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_run_id": { + "name": "active_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_run_status": { + "name": "active_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_hold_members_hold_issue_uq": { + "name": "issue_tree_hold_members_hold_issue_uq", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_company_issue_idx": { + "name": "issue_tree_hold_members_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_hold_depth_idx": { + "name": "issue_tree_hold_members_hold_depth_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_hold_members_company_id_companies_id_fk": { + "name": "issue_tree_hold_members_company_id_companies_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk": { + "name": "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issue_tree_holds", + "columnsFrom": [ + "hold_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_parent_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_parent_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_assignee_agent_id_agents_id_fk": { + "name": "issue_tree_hold_members_assignee_agent_id_agents_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "active_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_holds": { + "name": "issue_tree_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_issue_id": { + "name": "root_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_policy": { + "name": "release_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_actor_type": { + "name": "released_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_agent_id": { + "name": "released_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_run_id": { + "name": "released_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_metadata": { + "name": "release_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_holds_company_root_status_idx": { + "name": "issue_tree_holds_company_root_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_holds_company_status_mode_idx": { + "name": "issue_tree_holds_company_status_mode_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_holds_root_issue_id_issues_id_fk": { + "name": "issue_tree_holds_root_issue_id_issues_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "issues", + "columnsFrom": [ + "root_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_released_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "released_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "released_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_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 + }, + "origin_fingerprint": { + "name": "origin_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_next_check_at": { + "name": "monitor_next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_wake_requested_at": { + "name": "monitor_wake_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_last_triggered_at": { + "name": "monitor_last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_attempt_count": { + "name": "monitor_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "monitor_notes": { + "name": "monitor_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_scheduled_by": { + "name": "monitor_scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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_company_monitor_due_idx": { + "name": "issues_company_monitor_due_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "monitor_next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_incident_uq": { + "name": "issues_active_liveness_recovery_incident_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_leaf_uq": { + "name": "issues_active_liveness_recovery_leaf_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_fingerprint\" <> 'default'\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stale_run_evaluation_uq": { + "name": "issues_active_stale_run_evaluation_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stale_active_run_evaluation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_productivity_review_uq": { + "name": "issues_active_productivity_review_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'issue_productivity_review'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stranded_issue_recovery_uq": { + "name": "issues_active_stranded_issue_recovery_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stranded_issue_recovery'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_user_uq": { + "name": "join_requests_pending_human_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_email_uq": { + "name": "join_requests_pending_human_email_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"request_email_snapshot\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_database_namespaces": { + "name": "plugin_database_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_mode": { + "name": "namespace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schema'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_database_namespaces_plugin_idx": { + "name": "plugin_database_namespaces_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "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_managed_resources": { + "name": "plugin_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_managed_resources_company_idx": { + "name": "plugin_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_resource_idx": { + "name": "plugin_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_company_plugin_resource_uq": { + "name": "plugin_managed_resources_company_plugin_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_key": { + "name": "migration_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_migrations_plugin_key_idx": { + "name": "plugin_migrations_plugin_key_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_revisions": { + "name": "routine_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "restored_from_revision_id": { + "name": "restored_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_revisions_routine_revision_uq": { + "name": "routine_revisions_routine_revision_uq", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_routine_created_idx": { + "name": "routine_revisions_company_routine_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_revisions_company_id_companies_id_fk": { + "name": "routine_revisions_company_id_companies_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_routine_id_routines_id_fk": { + "name": "routine_revisions_routine_id_routines_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_restored_from_revision_id_routine_revisions_id_fk": { + "name": "routine_revisions_restored_from_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routine_revisions", + "columnsFrom": [ + "restored_from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_agent_id_agents_id_fk": { + "name": "routine_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "routine_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatch_fingerprint": { + "name": "dispatch_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_dispatch_fingerprint_idx": { + "name": "routine_runs_dispatch_fingerprint_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatch_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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 + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 5804f48ed28..4688f5cd316 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -547,6 +547,13 @@ "when": 1777933347806, "tag": "0077_unusual_karnak", "breakpoints": true + }, + { + "idx": 78, + "version": "7", + "when": 1778004024976, + "tag": "0078_white_darwin", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/issue_comments.ts b/packages/db/src/schema/issue_comments.ts index a431589a3e4..f1029890163 100644 --- a/packages/db/src/schema/issue_comments.ts +++ b/packages/db/src/schema/issue_comments.ts @@ -1,4 +1,5 @@ -import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core"; +import type { IssueCommentAuthorType, IssueCommentMetadata, IssueCommentPresentation } from "@paperclipai/shared"; +import { pgTable, uuid, text, timestamp, index, jsonb } from "drizzle-orm/pg-core"; import { companies } from "./companies.js"; import { issues } from "./issues.js"; import { agents } from "./agents.js"; @@ -12,8 +13,11 @@ 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"), + authorType: text("author_type").$type(), createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }), body: text("body").notNull(), + presentation: jsonb("presentation").$type(), + metadata: jsonb("metadata").$type(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index 30cab5a0be7..cfbadd798a6 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -1260,9 +1260,12 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { id: randomUUID(), companyId: parentIssue.companyId, issueId, + authorType: options?.authorAgentId ? "agent" : "system", authorAgentId: options?.authorAgentId ?? null, authorUserId: null, body, + presentation: null, + metadata: null, createdAt: now, updatedAt: now, }; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 001ccee26f2..7a63c4397cc 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -148,6 +148,25 @@ export const ISSUE_PRIORITIES = ["critical", "high", "medium", "low"] as const; export type IssuePriority = (typeof ISSUE_PRIORITIES)[number]; export const MAX_ISSUE_REQUEST_DEPTH = 1024; +export const ISSUE_COMMENT_AUTHOR_TYPES = ["user", "agent", "system"] as const; +export type IssueCommentAuthorType = (typeof ISSUE_COMMENT_AUTHOR_TYPES)[number]; + +export const ISSUE_COMMENT_PRESENTATION_KINDS = ["message", "system_notice"] as const; +export type IssueCommentPresentationKind = (typeof ISSUE_COMMENT_PRESENTATION_KINDS)[number]; + +export const ISSUE_COMMENT_PRESENTATION_TONES = ["neutral", "info", "success", "warning", "danger"] as const; +export type IssueCommentPresentationTone = (typeof ISSUE_COMMENT_PRESENTATION_TONES)[number]; + +export const ISSUE_COMMENT_METADATA_ROW_TYPES = [ + "text", + "code", + "key_value", + "issue_link", + "agent_link", + "run_link", +] as const; +export type IssueCommentMetadataRowType = (typeof ISSUE_COMMENT_METADATA_ROW_TYPES)[number]; + export function clampIssueRequestDepth(value: number | null | undefined): number { if (typeof value !== "number" || !Number.isFinite(value)) return 0; return Math.min(MAX_ISSUE_REQUEST_DEPTH, Math.max(0, Math.floor(value))); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6c0217430c5..f4cdcdf2e3d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -20,6 +20,10 @@ export { INBOX_MINE_ISSUE_STATUS_FILTER, ISSUE_PRIORITIES, MAX_ISSUE_REQUEST_DEPTH, + ISSUE_COMMENT_AUTHOR_TYPES, + ISSUE_COMMENT_METADATA_ROW_TYPES, + ISSUE_COMMENT_PRESENTATION_KINDS, + ISSUE_COMMENT_PRESENTATION_TONES, clampIssueRequestDepth, ISSUE_THREAD_INTERACTION_KINDS, ISSUE_THREAD_INTERACTION_STATUSES, @@ -130,6 +134,10 @@ export { type AgentIconName, type IssueStatus, type IssuePriority, + type IssueCommentAuthorType, + type IssueCommentMetadataRowType, + type IssueCommentPresentationKind, + type IssueCommentPresentationTone, type IssueThreadInteractionKind, type IssueThreadInteractionStatus, type IssueThreadInteractionContinuationPolicy, @@ -352,6 +360,8 @@ export type { IssueBlockerAttentionState, IssueProductivityReview, IssueProductivityReviewTrigger, + SuccessfulRunHandoffState, + SuccessfulRunHandoffStateKind, IssueReferenceSource, IssueRelatedWorkItem, IssueRelatedWorkSummary, @@ -366,6 +376,16 @@ export type { IssueExecutionStagePrincipal, IssueExecutionDecision, IssueComment, + IssueCommentMetadata, + IssueCommentMetadataSection, + IssueCommentMetadataRow, + IssueCommentMetadataTextRow, + IssueCommentMetadataCodeRow, + IssueCommentMetadataKeyValueRow, + IssueCommentMetadataIssueLinkRow, + IssueCommentMetadataAgentLinkRow, + IssueCommentMetadataRunLinkRow, + IssueCommentPresentation, IssueThreadInteractionActorFields, SuggestedTaskDraft, SuggestTasksPayload, @@ -475,6 +495,7 @@ export type { CompanyPortabilityProjectWorkspaceManifestEntry, CompanyPortabilityIssueRoutineTriggerManifestEntry, CompanyPortabilityIssueRoutineManifestEntry, + CompanyPortabilityIssueCommentManifestEntry, CompanyPortabilityIssueManifestEntry, CompanyPortabilityManifest, CompanyPortabilityExportResult, @@ -685,6 +706,11 @@ export { issueReviewRequestSchema, issueExecutionWorkspaceSettingsSchema, checkoutIssueSchema, + issueCommentAuthorTypeSchema, + issueCommentPresentationSchema, + issueCommentMetadataRowSchema, + issueCommentMetadataSectionSchema, + issueCommentMetadataSchema, addIssueCommentSchema, issueThreadInteractionStatusSchema, issueThreadInteractionKindSchema, diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index 1b30713c76f..d398cfbdeb2 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -1,5 +1,7 @@ import type { AgentEnvConfig } from "./secrets.js"; import type { RoutineVariable } from "./routine.js"; +import type { IssueCommentAuthorType } from "../constants.js"; +import type { IssueCommentMetadata, IssueCommentPresentation } from "./issue.js"; export interface CompanyPortabilityInclude { company: boolean; @@ -94,6 +96,16 @@ export interface CompanyPortabilityIssueRoutineManifestEntry { triggers: CompanyPortabilityIssueRoutineTriggerManifestEntry[]; } +export interface CompanyPortabilityIssueCommentManifestEntry { + body: string; + authorType: IssueCommentAuthorType; + authorAgentSlug: string | null; + authorUserId: string | null; + presentation: IssueCommentPresentation | null; + metadata: IssueCommentMetadata | null; + createdAt: string | null; +} + export interface CompanyPortabilityIssueManifestEntry { slug: string; identifier: string | null; @@ -112,6 +124,7 @@ export interface CompanyPortabilityIssueManifestEntry { billingCode: string | null; executionWorkspaceSettings: Record | null; assigneeAdapterOverrides: Record | null; + comments: CompanyPortabilityIssueCommentManifestEntry[]; metadata: Record | null; } diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 6f90d3bf243..82088c7a4a7 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -140,6 +140,8 @@ export type { IssueBlockerAttentionState, IssueProductivityReview, IssueProductivityReviewTrigger, + SuccessfulRunHandoffState, + SuccessfulRunHandoffStateKind, IssueReferenceSource, IssueRelatedWorkItem, IssueRelatedWorkSummary, @@ -155,6 +157,16 @@ export type { IssueReviewRequest, IssueExecutionDecision, IssueComment, + IssueCommentMetadata, + IssueCommentMetadataSection, + IssueCommentMetadataRow, + IssueCommentMetadataTextRow, + IssueCommentMetadataCodeRow, + IssueCommentMetadataKeyValueRow, + IssueCommentMetadataIssueLinkRow, + IssueCommentMetadataAgentLinkRow, + IssueCommentMetadataRunLinkRow, + IssueCommentPresentation, IssueThreadInteractionActorFields, SuggestedTaskDraft, SuggestTasksPayload, @@ -296,6 +308,7 @@ export type { CompanyPortabilityProjectWorkspaceManifestEntry, CompanyPortabilityIssueRoutineTriggerManifestEntry, CompanyPortabilityIssueRoutineManifestEntry, + CompanyPortabilityIssueCommentManifestEntry, CompanyPortabilityIssueManifestEntry, CompanyPortabilityManifest, CompanyPortabilityExportResult, diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index 56e92d5a3d6..dd19f37f2d6 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -1,4 +1,8 @@ import type { + IssueCommentAuthorType, + IssueCommentMetadataRowType, + IssueCommentPresentationKind, + IssueCommentPresentationTone, IssueExecutionMonitorClearReason, IssueExecutionMonitorKind, IssueExecutionMonitorRecoveryPolicy, @@ -162,6 +166,18 @@ export interface IssueProductivityReview { updatedAt: Date; } +export type SuccessfulRunHandoffStateKind = "required" | "resolved" | "escalated"; + +export interface SuccessfulRunHandoffState { + state: SuccessfulRunHandoffStateKind; + required: boolean; + sourceRunId: string | null; + correctiveRunId: string | null; + assigneeAgentId: string | null; + detectedProgressSummary: string | null; + createdAt: Date | string | null; +} + export interface IssueRelation { id: string; companyId: string; @@ -324,6 +340,7 @@ export interface Issue { blocks?: IssueRelationIssueSummary[]; blockerAttention?: IssueBlockerAttention; productivityReview?: IssueProductivityReview | null; + successfulRunHandoff?: SuccessfulRunHandoffState | null; relatedWork?: IssueRelatedWorkSummary; referencedIssueIdentifiers?: string[]; planDocument?: IssueDocument | null; @@ -346,14 +363,83 @@ export interface IssueComment { id: string; companyId: string; issueId: string; + authorType: IssueCommentAuthorType; authorAgentId: string | null; authorUserId: string | null; body: string; + presentation: IssueCommentPresentation | null; + metadata: IssueCommentMetadata | null; followUpRequested?: boolean; createdAt: Date; updatedAt: Date; } +interface IssueCommentMetadataRowBase { + type: IssueCommentMetadataRowType; + label?: string | null; +} + +export interface IssueCommentMetadataTextRow extends IssueCommentMetadataRowBase { + type: "text"; + text: string; +} + +export interface IssueCommentMetadataCodeRow extends IssueCommentMetadataRowBase { + type: "code"; + code: string; + language?: string | null; +} + +export interface IssueCommentMetadataKeyValueRow extends IssueCommentMetadataRowBase { + type: "key_value"; + label: string; + value: string; +} + +export interface IssueCommentMetadataIssueLinkRow extends IssueCommentMetadataRowBase { + type: "issue_link"; + issueId?: string | null; + identifier?: string | null; + title?: string | null; +} + +export interface IssueCommentMetadataAgentLinkRow extends IssueCommentMetadataRowBase { + type: "agent_link"; + agentId: string; + name?: string | null; +} + +export interface IssueCommentMetadataRunLinkRow extends IssueCommentMetadataRowBase { + type: "run_link"; + runId: string; + title?: string | null; +} + +export type IssueCommentMetadataRow = + | IssueCommentMetadataTextRow + | IssueCommentMetadataCodeRow + | IssueCommentMetadataKeyValueRow + | IssueCommentMetadataIssueLinkRow + | IssueCommentMetadataAgentLinkRow + | IssueCommentMetadataRunLinkRow; + +export interface IssueCommentMetadataSection { + title?: string | null; + rows: IssueCommentMetadataRow[]; +} + +export interface IssueCommentMetadata { + version: 1; + sections: IssueCommentMetadataSection[]; +} + +export interface IssueCommentPresentation { + kind: IssueCommentPresentationKind; + tone: IssueCommentPresentationTone; + title?: string | null; + detailsDefaultOpen: boolean; +} + export interface IssueThreadInteractionActorFields { createdByAgentId?: string | null; createdByUserId?: string | null; diff --git a/packages/shared/src/validators/company-portability.ts b/packages/shared/src/validators/company-portability.ts index ec9df1c2adb..a1eb5b1f6ef 100644 --- a/packages/shared/src/validators/company-portability.ts +++ b/packages/shared/src/validators/company-portability.ts @@ -1,5 +1,10 @@ import { z } from "zod"; import { MAX_COMPANY_ATTACHMENT_MAX_BYTES } from "../constants.js"; +import { + issueCommentAuthorTypeSchema, + issueCommentMetadataSchema, + issueCommentPresentationSchema, +} from "./issue.js"; import { routineVariableSchema } from "./routine.js"; export const portabilityIncludeSchema = z @@ -131,6 +136,16 @@ export const portabilityIssueRoutineManifestEntrySchema = z.object({ triggers: z.array(portabilityIssueRoutineTriggerManifestEntrySchema).default([]), }); +export const portabilityIssueCommentManifestEntrySchema = z.object({ + body: z.string().min(1), + authorType: issueCommentAuthorTypeSchema, + authorAgentSlug: z.string().min(1).nullable(), + authorUserId: z.string().nullable(), + presentation: issueCommentPresentationSchema.nullable(), + metadata: issueCommentMetadataSchema.nullable(), + createdAt: z.string().datetime().nullable(), +}); + export const portabilityIssueManifestEntrySchema = z.object({ slug: z.string().min(1), identifier: z.string().min(1).nullable(), @@ -149,6 +164,7 @@ export const portabilityIssueManifestEntrySchema = z.object({ billingCode: z.string().nullable(), executionWorkspaceSettings: z.record(z.unknown()).nullable(), assigneeAdapterOverrides: z.record(z.unknown()).nullable(), + comments: z.array(portabilityIssueCommentManifestEntrySchema).default([]), metadata: z.record(z.unknown()).nullable(), }); diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index e5d36865ec2..afb7cebc3a0 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -157,6 +157,11 @@ export { issueReviewRequestSchema, issueExecutionWorkspaceSettingsSchema, checkoutIssueSchema, + issueCommentAuthorTypeSchema, + issueCommentPresentationSchema, + issueCommentMetadataRowSchema, + issueCommentMetadataSectionSchema, + issueCommentMetadataSchema, addIssueCommentSchema, issueThreadInteractionStatusSchema, issueThreadInteractionKindSchema, diff --git a/packages/shared/src/validators/issue.test.ts b/packages/shared/src/validators/issue.test.ts index 4117aa423c5..6b973f192f6 100644 --- a/packages/shared/src/validators/issue.test.ts +++ b/packages/shared/src/validators/issue.test.ts @@ -54,6 +54,46 @@ describe("issue validators", () => { expect(parsed.body).toBe("Progress update\n\nNext action."); }); + it("accepts structured issue comment presentation and metadata", () => { + const parsed = addIssueCommentSchema.parse({ + body: "Paperclip needs a disposition before this issue can continue.", + authorType: "system", + presentation: { + kind: "system_notice", + tone: "warning", + title: "Needs disposition", + }, + metadata: { + version: 1, + sections: [ + { + title: "Evidence", + rows: [ + { type: "key_value", label: "Cause", value: "successful_run_missing_state" }, + { type: "issue_link", label: "Source issue", identifier: "PAP-3440" }, + { type: "run_link", label: "Run", runId: "11111111-1111-4111-8111-111111111111" }, + ], + }, + ], + }, + }); + + expect(parsed.presentation?.detailsDefaultOpen).toBe(false); + expect(parsed.metadata?.sections[0]?.rows).toHaveLength(3); + }); + + it("rejects arbitrary issue comment metadata", () => { + const parsed = addIssueCommentSchema.safeParse({ + body: "Hidden details", + metadata: { + version: 1, + transcript: "raw log dump", + }, + }); + + expect(parsed.success).toBe(false); + }); + it("normalizes escaped line breaks in generated task drafts", () => { const parsed = suggestedTaskDraftSchema.parse({ clientKey: "task-1", diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts index 57ff440d2e5..b45fd8c1755 100644 --- a/packages/shared/src/validators/issue.ts +++ b/packages/shared/src/validators/issue.ts @@ -8,6 +8,10 @@ import { ISSUE_EXECUTION_POLICY_MODES, ISSUE_EXECUTION_STAGE_TYPES, ISSUE_EXECUTION_STATE_STATUSES, + ISSUE_COMMENT_AUTHOR_TYPES, + ISSUE_COMMENT_METADATA_ROW_TYPES, + ISSUE_COMMENT_PRESENTATION_KINDS, + ISSUE_COMMENT_PRESENTATION_TONES, ISSUE_MONITOR_SCHEDULED_BY, ISSUE_PRIORITIES, clampIssueRequestDepth, @@ -233,8 +237,95 @@ export const checkoutIssueSchema = z.object({ export type CheckoutIssue = z.infer; +const commentMetadataLabelSchema = z.string().trim().min(1).max(120); +const commentMetadataTextSchema = z.string().trim().min(1).max(2000); + +export const issueCommentAuthorTypeSchema = z.enum(ISSUE_COMMENT_AUTHOR_TYPES); + +export const issueCommentPresentationSchema = z.object({ + kind: z.enum(ISSUE_COMMENT_PRESENTATION_KINDS).default("message"), + tone: z.enum(ISSUE_COMMENT_PRESENTATION_TONES).default("neutral"), + title: z.string().trim().min(1).max(160).nullable().optional(), + detailsDefaultOpen: z.boolean().optional().default(false), +}).strict(); + +export type IssueCommentPresentation = z.infer; + +const issueCommentMetadataBaseRowSchema = z.object({ + type: z.enum(ISSUE_COMMENT_METADATA_ROW_TYPES), + label: commentMetadataLabelSchema.nullable().optional(), +}); + +const issueCommentMetadataTextRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("text"), + text: commentMetadataTextSchema, +}).strict(); + +const issueCommentMetadataCodeRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("code"), + code: z.string().min(1).max(4000), + language: z.string().trim().min(1).max(40).nullable().optional(), +}).strict(); + +const issueCommentMetadataKeyValueRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("key_value"), + label: commentMetadataLabelSchema, + value: commentMetadataTextSchema, +}).strict(); + +const issueCommentMetadataIssueLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("issue_link"), + issueId: z.string().uuid().nullable().optional(), + identifier: z.string().trim().min(1).max(80).nullable().optional(), + title: z.string().trim().min(1).max(240).nullable().optional(), +}).strict(); + +const issueCommentMetadataAgentLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("agent_link"), + agentId: z.string().uuid(), + name: z.string().trim().min(1).max(160).nullable().optional(), +}).strict(); + +const issueCommentMetadataRunLinkRowSchema = issueCommentMetadataBaseRowSchema.extend({ + type: z.literal("run_link"), + runId: z.string().uuid(), + title: z.string().trim().min(1).max(160).nullable().optional(), +}).strict(); + +export const issueCommentMetadataRowSchema = z.discriminatedUnion("type", [ + issueCommentMetadataTextRowSchema, + issueCommentMetadataCodeRowSchema, + issueCommentMetadataKeyValueRowSchema, + issueCommentMetadataIssueLinkRowSchema, + issueCommentMetadataAgentLinkRowSchema, + issueCommentMetadataRunLinkRowSchema, +]).superRefine((value, ctx) => { + if (value.type === "issue_link" && !value.issueId && !value.identifier) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Issue link rows require issueId or identifier", + path: ["issueId"], + }); + } +}); + +export const issueCommentMetadataSectionSchema = z.object({ + title: z.string().trim().min(1).max(160).nullable().optional(), + rows: z.array(issueCommentMetadataRowSchema).min(1).max(50), +}).strict(); + +export const issueCommentMetadataSchema = z.object({ + version: z.literal(1), + sections: z.array(issueCommentMetadataSectionSchema).min(1).max(20), +}).strict(); + +export type IssueCommentMetadata = z.infer; + export const addIssueCommentSchema = z.object({ body: multilineTextSchema.pipe(z.string().min(1)), + authorType: issueCommentAuthorTypeSchema.optional(), + presentation: issueCommentPresentationSchema.nullable().optional(), + metadata: issueCommentMetadataSchema.nullable().optional(), reopen: z.boolean().optional(), resume: z.boolean().optional(), interrupt: z.boolean().optional(), diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index fecd7a78357..ff65bbee093 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -35,9 +35,11 @@ const projectSvc = { const issueSvc = { list: vi.fn(), + listComments: vi.fn(), getById: vi.fn(), getByIdentifier: vi.fn(), create: vi.fn(), + addComment: vi.fn(), }; const routineSvc = { @@ -131,6 +133,14 @@ describe("company portability", () => { config, secretKeys: new Set(), })); + issueSvc.listComments.mockResolvedValue([]); + issueSvc.addComment.mockResolvedValue({ + id: "comment-imported", + body: "Imported comment", + authorType: "system", + presentation: null, + metadata: null, + }); companySvc.getById.mockResolvedValue({ id: "company-1", name: "Paperclip", @@ -2333,6 +2343,103 @@ describe("company portability", () => { expect(materializedFiles["AGENTS.md"]).not.toContain('name: "ClaudeCoder"'); }); + it("does not silently add local adapter permission bypasses on import", async () => { + const portability = companyPortabilityService({} as any); + + companySvc.create.mockResolvedValue({ + id: "company-imported", + name: "Imported Paperclip", + }); + accessSvc.ensureMembership.mockResolvedValue(undefined); + agentSvc.create.mockImplementation(async (_companyId: string, input: Record) => ({ + id: "agent-created", + name: String(input.name), + adapterType: input.adapterType, + adapterConfig: input.adapterConfig, + })); + + const exported = await portability.exportBundle("company-1", { + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + }); + + agentSvc.list.mockResolvedValue([]); + + await portability.importBundle({ + source: { + type: "inline", + rootPath: exported.rootPath, + files: exported.files, + }, + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + target: { + mode: "new_company", + newCompanyName: "Imported Paperclip", + }, + agents: ["claudecoder"], + collisionStrategy: "rename", + }, "user-1"); + + expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ + adapterType: "claude_local", + adapterConfig: expect.not.objectContaining({ + dangerouslySkipPermissions: expect.anything(), + }), + })); + + await portability.importBundle({ + source: { + type: "inline", + rootPath: exported.rootPath, + files: exported.files, + }, + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + target: { + mode: "new_company", + newCompanyName: "Imported Paperclip", + }, + agents: ["claudecoder"], + collisionStrategy: "rename", + adapterOverrides: { + claudecoder: { + adapterType: "codex_local", + adapterConfig: { + extraArgs: [], + args: ["--legacy-arg"], + }, + }, + }, + }, "user-1"); + + expect(agentSvc.create).toHaveBeenLastCalledWith("company-imported", expect.objectContaining({ + adapterType: "codex_local", + adapterConfig: expect.objectContaining({ + extraArgs: ["--skip-git-repo-check"], + args: ["--legacy-arg"], + }), + })); + expect(agentSvc.create).toHaveBeenLastCalledWith("company-imported", expect.objectContaining({ + adapterConfig: expect.not.objectContaining({ + dangerouslyBypassApprovalsAndSandbox: expect.anything(), + dangerouslyBypassSandbox: expect.anything(), + }), + })); + }); + it("preserves issue labelIds through export and import round-trip", async () => { const portability = companyPortabilityService({} as any); @@ -2399,6 +2506,85 @@ describe("company portability", () => { ); }); + it("preserves issue comment presentation fields through export and import", async () => { + const portability = companyPortabilityService({} as any); + const presentation = { kind: "system_notice", tone: "warning", detailsDefaultOpen: false }; + const metadata = { + version: 1, + sections: [{ rows: [{ type: "key_value", label: "Cause", value: "successful_run_missing_state" }] }], + }; + + projectSvc.list.mockResolvedValue([]); + projectSvc.listWorkspaces.mockResolvedValue([]); + issueSvc.list.mockResolvedValue([ + { + id: "issue-1", + identifier: "PAP-1", + title: "Needs disposition", + description: "System notice source", + projectId: null, + projectWorkspaceId: null, + assigneeAgentId: null, + status: "todo", + priority: "high", + labelIds: [], + billingCode: null, + executionWorkspaceSettings: null, + assigneeAdapterOverrides: null, + }, + ]); + issueSvc.listComments.mockResolvedValue([ + { + id: "comment-1", + issueId: "issue-1", + companyId: "company-1", + authorType: "system", + authorAgentId: null, + authorUserId: null, + body: "Paperclip needs a disposition before this issue can continue.", + presentation, + metadata, + createdAt: new Date("2026-05-04T12:00:00.000Z"), + updatedAt: new Date("2026-05-04T12:00:00.000Z"), + }, + ]); + + const exported = await portability.exportBundle("company-1", { + include: { company: true, agents: false, projects: false, issues: true }, + }); + + const extension = asTextFile(exported.files[".paperclip.yaml"]); + expect(extension).toContain("comments:"); + expect(extension).toContain("system_notice"); + expect(extension).toContain("successful_run_missing_state"); + + companySvc.create.mockResolvedValue({ id: "company-imported", name: "Imported" }); + accessSvc.ensureMembership.mockResolvedValue(undefined); + agentSvc.list.mockResolvedValue([]); + projectSvc.list.mockResolvedValue([]); + issueSvc.create.mockResolvedValue({ id: "issue-imported", title: "Needs disposition" }); + + await portability.importBundle({ + source: { type: "inline", rootPath: exported.rootPath, files: exported.files }, + include: { company: true, agents: false, projects: false, issues: true }, + target: { mode: "new_company", newCompanyName: "Imported" }, + agents: "all", + collisionStrategy: "rename", + }, "user-1"); + + expect(issueSvc.addComment).toHaveBeenCalledWith( + "issue-imported", + "Paperclip needs a disposition before this issue can continue.", + { agentId: undefined, userId: undefined }, + { + authorType: "system", + presentation, + metadata, + createdAt: "2026-05-04T12:00:00.000Z", + }, + ); + }); + it("strips root AGENTS frontmatter when importing a nested agent entry path", async () => { const portability = companyPortabilityService({} as any); diff --git a/server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts b/server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts index 45ba2c5d655..166f8fc413a 100644 --- a/server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts +++ b/server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts @@ -208,6 +208,7 @@ describeEmbeddedPostgres("active-run output watchdog", () => { expect(evaluations[0]).toMatchObject({ priority: "medium", assigneeAgentId: managerId, + assigneeAdapterOverrides: { modelProfile: "cheap" }, originId: runId, originFingerprint: `stale_active_run:${companyId}:${runId}`, }); diff --git a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts index de1b80056ef..6cda7fb1743 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -13,6 +13,7 @@ import { issues, } from "@paperclipai/db"; import { heartbeatService } from "../services/heartbeat.ts"; +import { SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY } from "../services/recovery/index.ts"; import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.ts"; async function waitFor(condition: () => boolean | Promise, timeoutMs = 10_000, intervalMs = 50) { @@ -543,8 +544,24 @@ describe("heartbeat comment wake batching", () => { .values({ companyId, issueId, + authorType: "user", authorUserId: "user-1", body: "Queued follow-up", + presentation: { + kind: "system_notice", + tone: "warning", + detailsDefaultOpen: false, + }, + metadata: { + version: 1, + sections: [ + { + rows: [ + { type: "key_value", label: "Cause", value: "successful_run_missing_state" }, + ], + }, + ], + }, }) .returning() .then((rows) => rows[0]); @@ -577,7 +594,15 @@ describe("heartbeat comment wake batching", () => { comments: [ expect.objectContaining({ id: queuedComment.id, + authorType: "user", body: "Queued follow-up", + presentation: expect.objectContaining({ + kind: "system_notice", + tone: "warning", + }), + metadata: expect.objectContaining({ + version: 1, + }), }), ], commentWindow: { @@ -1130,6 +1155,7 @@ describe("heartbeat comment wake batching", () => { expect(payloads).toHaveLength(2); expect(runs[1]?.contextSnapshot).toMatchObject({ retryReason: "missing_issue_comment", + modelProfile: "cheap", }); } finally { gateway.releaseFirstWait(); @@ -1329,8 +1355,9 @@ describe("heartbeat comment wake batching", () => { eq(agentWakeupRequests.agentId, primaryAgentId), eq(agentWakeupRequests.reason, "missing_issue_comment"), ), - ); + ); expect(missingCommentRetries).toHaveLength(1); + expect(missingCommentRetries[0]?.payload).toMatchObject({ modelProfile: "cheap" }); } finally { gateway.releaseFirstWait(); await gateway.close(); @@ -1566,7 +1593,8 @@ describe("heartbeat comment wake batching", () => { .select() .from(heartbeatRuns) .where(eq(heartbeatRuns.agentId, agentId)); - return runs.length === 1 && runs[0]?.status === "succeeded" && runs[0]?.issueCommentStatus === "satisfied"; + const sourceRun = runs.find((run) => run.id === firstRun?.id); + return sourceRun?.status === "succeeded" && sourceRun.issueCommentStatus === "satisfied"; }); const runs = await db @@ -1574,9 +1602,26 @@ describe("heartbeat comment wake batching", () => { .from(heartbeatRuns) .where(eq(heartbeatRuns.agentId, agentId)); - expect(runs).toHaveLength(1); - expect(runs[0]?.issueCommentStatus).toBe("satisfied"); - expect(runs[0]?.issueCommentSatisfiedByCommentId).not.toBeNull(); + const sourceRun = runs.find((run) => run.id === firstRun?.id); + expect(sourceRun?.issueCommentStatus).toBe("satisfied"); + expect(sourceRun?.issueCommentSatisfiedByCommentId).not.toBeNull(); + + await waitFor(async () => { + const comments = await db + .select() + .from(issueComments) + .where(eq(issueComments.issueId, issueId)); + const wakeups = await db + .select() + .from(agentWakeupRequests) + .where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))); + + const hasHandoffComment = comments.some((comment) => + comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY + ); + const hasHandoffWake = wakeups.some((wakeup) => wakeup.reason === "finish_successful_run_handoff"); + return hasHandoffComment && hasHandoffWake; + }); const comments = await db .select() @@ -1584,16 +1629,19 @@ describe("heartbeat comment wake batching", () => { .where(eq(issueComments.issueId, issueId)) .orderBy(asc(issueComments.createdAt)); - expect(comments).toHaveLength(1); - expect(comments[0]?.body).toBe("Manual completion comment from the run."); - expect(comments[0]?.createdByRunId).toBe(firstRun?.id); + expect(comments.some((comment) => comment.body === "Manual completion comment from the run.")).toBe(true); + expect(comments.some((comment) => + comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY + )).toBe(true); + expect(comments.every((comment) => !comment.body.startsWith("## Run summary"))).toBe(true); const wakeups = await db .select() .from(agentWakeupRequests) .where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))); - expect(wakeups).toHaveLength(1); + expect(wakeups.some((wakeup) => wakeup.reason === "missing_issue_comment")).toBe(false); + expect(wakeups.some((wakeup) => wakeup.reason === "finish_successful_run_handoff")).toBe(true); } finally { gateway.releaseFirstWait(); await gateway.close(); diff --git a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts index ec5430e2a0c..ba699208aa0 100644 --- a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts +++ b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts @@ -280,6 +280,23 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = unresolvedBlockerIssueIds: [blockerId], }); + let finishReadyRun!: () => void; + const readyRunCanFinish = new Promise((resolve) => { + finishReadyRun = resolve; + }); + mockAdapterExecute.mockImplementationOnce(async () => { + await readyRunCanFinish; + return { + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Ready dependency scheduling run complete.", + provider: "test", + model: "test-model", + }; + }); + const readyWake = await heartbeat.wakeup(agentId, { source: "assignment", triggerDetail: "system", @@ -288,6 +305,15 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = contextSnapshot: { issueId: readyIssueId, wakeReason: "issue_assigned" }, }); expect(readyWake).not.toBeNull(); + await db.insert(issueComments).values({ + companyId, + issueId: readyIssueId, + authorAgentId: agentId, + authorType: "agent", + createdByRunId: readyWake!.id, + body: "Ready dependency scheduling run complete.", + }); + finishReadyRun(); await waitForCondition(async () => { const run = await db @@ -354,6 +380,14 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = expect(promotedBlockedRun?.status).toBe("succeeded"); expect(blockedWakeRequestCount).toBeGreaterThanOrEqual(2); + + const noActiveRuns = await waitForCondition(async () => { + const rows = await db + .select({ status: heartbeatRuns.status }) + .from(heartbeatRuns); + return rows.every((run) => run.status !== "queued" && run.status !== "running"); + }, 10_000); + expect(noActiveRuns).toBe(true); }); it("honors maxConcurrentRuns 1 by leaving a second assignment wake queued", async () => { @@ -429,6 +463,14 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = contextSnapshot: { issueId: firstIssueId, wakeReason: "issue_assigned" }, }); expect(firstWake).not.toBeNull(); + await db.insert(issueComments).values({ + companyId, + issueId: firstIssueId, + authorAgentId: agentId, + authorType: "agent", + createdByRunId: firstWake!.id, + body: "First assignment run completed.", + }); const firstRunStarted = await waitForCondition(async () => { const run = await db @@ -439,7 +481,7 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = return run?.status === "running"; }); expect(firstRunStarted).toBe(true); - const firstAdapterStarted = await waitForCondition(async () => mockAdapterExecute.mock.calls.length === 1); + const firstAdapterStarted = await waitForCondition(async () => mockAdapterExecute.mock.calls.length === 1, 30_000); expect(firstAdapterStarted).toBe(true); const secondWake = await heartbeat.wakeup(agentId, { @@ -450,6 +492,14 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = contextSnapshot: { issueId: secondIssueId, wakeReason: "issue_assigned" }, }); expect(secondWake).not.toBeNull(); + await db.insert(issueComments).values({ + companyId, + issueId: secondIssueId, + authorAgentId: agentId, + authorType: "agent", + createdByRunId: secondWake!.id, + body: "Second assignment run completed.", + }); const secondRunWhileFirstRunning = await db .select({ status: heartbeatRuns.status }) @@ -470,11 +520,11 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = return run?.status === "succeeded"; }); expect(secondRunSucceeded).toBe(true); - expect(mockAdapterExecute).toHaveBeenCalledTimes(2); + expect(mockAdapterExecute.mock.calls.length).toBeGreaterThanOrEqual(2); } finally { finishFirstRun(); } - }); + }, 40_000); it("cancels stale queued runs when issue blockers are still unresolved", async () => { const companyId = randomUUID(); @@ -598,6 +648,14 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = .update(agentWakeupRequests) .set({ runId: readyRunId }) .where(eq(agentWakeupRequests.id, readyWakeupRequestId)); + await db.insert(issueComments).values({ + companyId, + issueId: readyIssueId, + authorAgentId: agentId, + authorType: "agent", + createdByRunId: readyRunId, + body: "Ready queued run completed.", + }); await db .update(issues) .set({ @@ -665,7 +723,7 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = executionLockedAt: null, }); expect(readyRun?.status).toBe("succeeded"); - expect(mockAdapterExecute).toHaveBeenCalledTimes(1); + expect(mockAdapterExecute).toHaveBeenCalledTimes(2); }); it("suppresses normal wakeups while allowing comment interaction wakes under a pause hold", async () => { diff --git a/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts b/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts index 10ad103cd44..45c9647561e 100644 --- a/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts +++ b/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts @@ -320,6 +320,7 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => { expect(escalations[0]).toMatchObject({ parentId: blockerIssueId, assigneeAgentId: managerId, + assigneeAdapterOverrides: { modelProfile: "cheap" }, status: expect.stringMatching(/^(todo|in_progress|done)$/), originFingerprint: [ "harness_liveness_leaf", @@ -568,6 +569,7 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => { executionWorkspaceId: null, executionWorkspacePreference: null, assigneeAgentId: managerId, + assigneeAdapterOverrides: { modelProfile: "cheap" }, }); }); diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index e572d0627f5..2b917879e30 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -23,6 +23,7 @@ import { issueRelations, issueTreeHoldMembers, issueTreeHolds, + issueWorkProducts, issues, } from "@paperclipai/db"; import { @@ -69,7 +70,15 @@ vi.mock("../adapters/index.ts", async () => { }; }); -import { heartbeatService } from "../services/heartbeat.ts"; +import { + heartbeatService, + redactDetectedSuccessfulRunProgressSummaryForBoard, +} from "../services/heartbeat.ts"; +import { + SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY, + SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY, + SUCCESSFUL_RUN_MISSING_STATE_REASON, +} from "../services/recovery/index.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -313,6 +322,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { await db.delete(costEvents); await db.delete(environmentLeases); await db.delete(environments); + await db.delete(issueWorkProducts); await db.delete(issueComments); await db.delete(issueDocuments); await db.delete(documentRevisions); @@ -709,6 +719,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { originId: input.issueId, originRunId: input.runId, priority: "medium", + assigneeAdapterOverrides: { modelProfile: "cheap" }, }); expect(recovery.title).toContain("Recover stalled issue"); expect(recovery.description).toContain(`Previous source status: \`${input.previousStatus}\``); @@ -743,6 +754,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { companyId: input.companyId, reason: "issue_assigned", source: "assignment", + payload: expect.objectContaining({ modelProfile: "cheap" }), }); const recoveryRun = recoveryWakeup?.runId @@ -758,6 +770,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { source: "stranded_issue_recovery", sourceIssueId: input.issueId, strandedRunId: input.runId, + modelProfile: "cheap", }); return recovery; @@ -915,6 +928,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(retryRun?.status).toBe("queued"); expect(retryRun?.retryOfRunId).toBe(runId); expect(retryRun?.processLossRetryCount).toBe(1); + expect(retryRun?.contextSnapshot).toMatchObject({ modelProfile: "cheap" }); const issue = await db .select() @@ -1227,7 +1241,10 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect((failedRun?.resultJson as Record | null)?.errorFamily).toBe("transient_upstream"); expect(retryRun?.status).toBe("scheduled_retry"); expect(retryRun?.scheduledRetryReason).toBe("transient_failure"); - expect((retryRun?.contextSnapshot as Record | null)?.codexTransientFallbackMode).toBe("same_session"); + expect(retryRun?.contextSnapshot).toMatchObject({ + codexTransientFallbackMode: "same_session", + modelProfile: "cheap", + }); const issue = await db .select() @@ -1241,6 +1258,448 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(comments).toHaveLength(0); }); + it("queues one finish-handoff wake when a successful run leaves in-progress work without a next action", async () => { + const { companyId, agentId, runId, issueId } = await seedQueuedIssueRunFixture(); + mockAdapterExecute.mockImplementationOnce(async (ctx: { runId: string }) => { + await db.insert(issueComments).values({ + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: ctx.runId, + body: "Implemented the backend detector, but did not choose a final issue state.", + }); + return { + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Implemented the backend detector, but did not choose a final issue state.", + provider: "test", + model: "test-model", + }; + }); + const heartbeat = heartbeatService(db); + + await heartbeat.resumeQueuedRuns(); + await waitForRunToSettle(heartbeat, runId, 5_000); + + const handoffWakeups = await waitForValue(async () => { + const rows = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.agentId, agentId)); + const matches = rows.filter((wakeup) => wakeup.reason === "finish_successful_run_handoff"); + return matches.length > 0 ? matches : null; + }, 5_000); + await waitForHeartbeatIdle(db, 5_000); + + expect(handoffWakeups).toHaveLength(1); + expect(handoffWakeups[0]?.idempotencyKey).toBe(`finish_successful_run_handoff:${issueId}:${runId}:1`); + expect(handoffWakeups[0]?.payload).toMatchObject({ + issueId, + sourceRunId: runId, + handoffRequired: true, + handoffReason: "successful_run_missing_state", + handoffAttempt: 1, + maxHandoffAttempts: 1, + resumeIntent: true, + resumeFromRunId: runId, + }); + + const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); + const handoffComment = comments.find((comment) => comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY); + expect(handoffComment).toBeTruthy(); + expect(handoffComment?.authorType).toBe("system"); + expect(handoffComment?.presentation).toMatchObject({ + kind: "system_notice", + tone: "warning", + detailsDefaultOpen: false, + }); + expect(handoffComment?.metadata).toMatchObject({ + version: 1, + sections: expect.arrayContaining([ + expect.objectContaining({ + title: "Required action", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "key_value", label: "Missing disposition", value: "clear_next_step" }), + ]), + }), + expect.objectContaining({ + title: "Run evidence", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "run_link", runId }), + expect.objectContaining({ type: "key_value", label: "Normalized cause", value: SUCCESSFUL_RUN_MISSING_STATE_REASON }), + ]), + }), + ]), + }); + + const activity = await db + .select() + .from(activityLog) + .where(eq(activityLog.entityId, issueId)); + expect(activity.some((event) => event.action === "issue.successful_run_handoff_required")).toBe(true); + }); + + it("requeues a missing-disposition handoff when the previous corrective wake was cancelled", async () => { + const { companyId, agentId, runId, issueId } = await seedQueuedIssueRunFixture(); + const idempotencyKey = `finish_successful_run_handoff:${issueId}:${runId}:1`; + await db.insert(agentWakeupRequests).values({ + id: randomUUID(), + companyId, + agentId, + source: "automation", + triggerDetail: "system", + reason: "finish_successful_run_handoff", + payload: { + issueId, + sourceRunId: runId, + handoffRequired: true, + handoffReason: SUCCESSFUL_RUN_MISSING_STATE_REASON, + }, + status: "cancelled", + idempotencyKey, + requestedAt: new Date("2026-03-19T00:00:01.000Z"), + finishedAt: new Date("2026-03-19T00:00:02.000Z"), + updatedAt: new Date("2026-03-19T00:00:02.000Z"), + }); + mockAdapterExecute.mockImplementationOnce(async (ctx: { runId: string }) => { + await db.insert(issueComments).values({ + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: ctx.runId, + body: "Implemented recovery handling, but did not choose a final issue state.", + }); + return { + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Implemented recovery handling, but did not choose a final issue state.", + provider: "test", + model: "test-model", + }; + }); + const heartbeat = heartbeatService(db); + + await heartbeat.resumeQueuedRuns(); + await waitForRunToSettle(heartbeat, runId, 5_000); + + const handoffWakeups = await waitForValue(async () => { + const rows = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.idempotencyKey, idempotencyKey)); + const requeued = rows.filter((wakeup) => wakeup.reason === "finish_successful_run_handoff"); + return requeued.length > 1 ? requeued : null; + }, 5_000); + await waitForHeartbeatIdle(db, 5_000); + + expect(handoffWakeups).toHaveLength(2); + expect(handoffWakeups.filter((wakeup) => wakeup.status === "cancelled")).toHaveLength(1); + expect(handoffWakeups.some((wakeup) => wakeup.status !== "cancelled")).toBe(true); + }); + + it("queues one missing-disposition handoff for artifact-producing successful runs left in progress", async () => { + const { companyId, agentId, runId, issueId } = await seedQueuedIssueRunFixture(); + mockAdapterExecute.mockImplementationOnce(async (ctx: { runId: string }) => { + const documentId = randomUUID(); + const revisionId = randomUUID(); + await db.insert(issueComments).values({ + companyId, + issueId, + authorAgentId: agentId, + createdByRunId: ctx.runId, + body: "Drafted the Phase 3 test plan but did not choose a final issue disposition.", + }); + await db.insert(documents).values({ + id: documentId, + companyId, + title: "Regression test plan", + format: "markdown", + latestBody: "# Regression test plan\n\n- Cover artifact-producing successful runs", + latestRevisionId: revisionId, + latestRevisionNumber: 1, + createdByAgentId: agentId, + updatedByAgentId: agentId, + }); + await db.insert(documentRevisions).values({ + id: revisionId, + companyId, + documentId, + revisionNumber: 1, + title: "Regression test plan", + format: "markdown", + body: "# Regression test plan\n\n- Cover artifact-producing successful runs", + createdByAgentId: agentId, + createdByRunId: ctx.runId, + }); + await db.insert(issueDocuments).values({ + companyId, + issueId, + documentId, + key: "plan", + }); + await db.insert(issueWorkProducts).values({ + companyId, + issueId, + type: "report", + provider: "test", + externalId: "phase-3-report", + title: "Phase 3 regression notes", + status: "ready", + summary: "Successful run produced a visible artifact.", + createdByRunId: ctx.runId, + }); + return { + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Created comments, a plan document, and a work product without choosing a disposition.", + provider: "test", + model: "test-model", + }; + }); + const heartbeat = heartbeatService(db); + + await heartbeat.resumeQueuedRuns(); + const settledRun = await waitForRunToSettle(heartbeat, runId, 5_000); + + const handoffWakeups = await waitForValue(async () => { + const rows = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.agentId, agentId)); + const matches = rows.filter((wakeup) => wakeup.reason === "finish_successful_run_handoff"); + return matches.length > 0 ? matches : null; + }, 5_000); + await waitForHeartbeatIdle(db, 5_000); + const classifiedRun = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, runId)) + .then((rows) => rows[0] ?? null); + + expect(classifiedRun?.status ?? settledRun?.status).toBe("succeeded"); + expect(classifiedRun?.livenessState).toBe("advanced"); + expect(handoffWakeups).toHaveLength(1); + expect(handoffWakeups[0]?.idempotencyKey).toBe(`finish_successful_run_handoff:${issueId}:${runId}:1`); + + const issue = await db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0] ?? null); + expect(issue?.status).toBe("in_progress"); + await expect(sourceBlockerIssueIds(companyId, issueId)).resolves.toEqual([]); + + const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); + expect(comments.filter((comment) => comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY)).toHaveLength(1); + expect(comments.some((comment) => comment.body.startsWith("Drafted the Phase 3 test plan"))).toBe(true); + + const workProducts = await db.select().from(issueWorkProducts).where(eq(issueWorkProducts.issueId, issueId)); + expect(workProducts).toHaveLength(1); + const recoveryIssues = await db + .select() + .from(issues) + .where(and(eq(issues.companyId, companyId), eq(issues.originKind, "stranded_issue_recovery"))); + expect(recoveryIssues).toHaveLength(0); + }); + + it("redacts secret-bearing successful-run detected progress before handoff disclosure", async () => { + const { agentId, runId, issueId } = await seedQueuedIssueRunFixture(); + const bearerSecret = "live-bearer-token-value"; + const apiKeySecret = "sk-testsuccessfulhandoffsecret"; + const redactedDetectedSummary = redactDetectedSuccessfulRunProgressSummaryForBoard( + `Next action noted: Authorization: Bearer ${bearerSecret} OPENAI_API_KEY=${apiKeySecret}`, + { enabled: false }, + ); + expect(redactedDetectedSummary).toContain("***REDACTED***"); + expect(redactedDetectedSummary).not.toContain(bearerSecret); + expect(redactedDetectedSummary).not.toContain(apiKeySecret); + + mockAdapterExecute.mockResolvedValueOnce({ + exitCode: 0, + signal: null, + timedOut: false, + errorMessage: null, + summary: "Made progress but left the issue open.", + resultJson: { + message: `Next action: Authorization: Bearer ${bearerSecret} OPENAI_API_KEY=${apiKeySecret}`, + }, + provider: "test", + model: "test-model", + }); + const heartbeat = heartbeatService(db); + + await heartbeat.resumeQueuedRuns(); + await waitForRunToSettle(heartbeat, runId, 5_000); + + const handoffWakeups = await waitForValue(async () => { + const rows = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.agentId, agentId)); + const matches = rows.filter((wakeup) => wakeup.reason === "finish_successful_run_handoff"); + return matches.length > 0 ? matches : null; + }, 5_000); + await waitForHeartbeatIdle(db, 5_000); + + expect(handoffWakeups).toHaveLength(1); + const wakeupPayloadText = JSON.stringify(handoffWakeups[0]?.payload ?? {}); + expect(wakeupPayloadText).not.toContain(bearerSecret); + expect(wakeupPayloadText).not.toContain(apiKeySecret); + + const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); + const handoffComment = comments.find((comment) => comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY); + expect(handoffComment).toBeTruthy(); + expect(handoffComment?.body).not.toContain(bearerSecret); + expect(handoffComment?.body).not.toContain(apiKeySecret); + expect(JSON.stringify(handoffComment?.metadata ?? {})).not.toContain(bearerSecret); + expect(JSON.stringify(handoffComment?.metadata ?? {})).not.toContain(apiKeySecret); + + const activity = await db + .select() + .from(activityLog) + .where(eq(activityLog.entityId, issueId)); + const handoffActivity = activity.find((event) => event.action === "issue.successful_run_handoff_required"); + expect(handoffActivity).toBeTruthy(); + const activityDetailsText = JSON.stringify(handoffActivity?.details ?? {}); + expect(activityDetailsText).not.toContain(bearerSecret); + expect(activityDetailsText).not.toContain(apiKeySecret); + }); + + it("escalates an exhausted failed successful-run handoff without using generic continuation recovery first", async () => { + const { companyId, agentId, runId, issueId } = await seedStrandedIssueFixture({ + status: "in_progress", + runStatus: "failed", + runErrorCode: "adapter_failed", + runError: "Authorization: Bearer sk-test-successful-handoff-secret", + }); + const sourceRunId = randomUUID(); + await db + .update(heartbeatRuns) + .set({ + contextSnapshot: { + issueId, + taskId: issueId, + wakeReason: "finish_successful_run_handoff", + sourceRunId, + resumeFromRunId: sourceRunId, + handoffRequired: true, + handoffReason: "successful_run_missing_state", + missingDisposition: "clear_next_step", + handoffAttempt: 1, + maxHandoffAttempts: 1, + }, + }) + .where(eq(heartbeatRuns.id, runId)); + const heartbeat = heartbeatService(db); + + const result = await heartbeat.reconcileStrandedAssignedIssues(); + expect(result.continuationRequeued).toBe(0); + expect(result.escalated).toBe(0); + expect(result.successfulRunHandoffEscalated).toBe(1); + expect(result.issueIds).toEqual([issueId]); + + const recovery = await waitForValue(async () => + db.select().from(issues).where( + and( + eq(issues.companyId, companyId), + eq(issues.originKind, "stranded_issue_recovery"), + eq(issues.originId, issueId), + ), + ).then((rows) => rows[0] ?? null), + ); + expect(recovery?.assigneeAgentId).toBe(agentId); + expect(recovery?.title).toContain("Recover missing next step"); + expect(recovery?.description).toContain("Normalized cause: `successful_run_missing_state`"); + expect(recovery?.description).toContain("not a runtime/adapter crash report"); + expect(recovery?.description).toContain(`Source run: [\`${sourceRunId}\`]`); + expect(recovery?.description).toContain("Missing disposition: `clear_next_step`"); + expect(recovery?.description).toContain("Source assignee: [CodexCoder]"); + expect(recovery?.description).not.toContain("sk-test-successful-handoff-secret"); + + const sourceIssue = await db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0] ?? null); + expect(sourceIssue?.status).toBe("blocked"); + await expect(sourceBlockerIssueIds(companyId, issueId)).resolves.toEqual([recovery?.id]); + + const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); + expect(comments[0]?.body).toBe(SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY); + expect(comments[0]?.authorType).toBe("system"); + expect(comments[0]?.presentation).toMatchObject({ + kind: "system_notice", + tone: "danger", + detailsDefaultOpen: false, + }); + expect(comments[0]?.metadata).toMatchObject({ + version: 1, + sections: expect.arrayContaining([ + expect.objectContaining({ + title: "Recovery owner", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "issue_link", identifier: recovery?.identifier }), + expect.objectContaining({ type: "agent_link", label: "Recovery owner", name: "CodexCoder" }), + ]), + }), + expect.objectContaining({ + title: "Run evidence", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "key_value", label: "Normalized cause", value: SUCCESSFUL_RUN_MISSING_STATE_REASON }), + expect.objectContaining({ type: "key_value", label: "Missing disposition", value: "clear_next_step" }), + ]), + }), + ]), + }); + expect(comments[0]?.body).not.toContain("sk-test-successful-handoff-secret"); + expect(JSON.stringify(comments[0]?.metadata ?? {})).not.toContain("sk-test-successful-handoff-secret"); + + const activity = await db.select().from(activityLog).where(eq(activityLog.entityId, issueId)); + expect(activity.some((event) => event.action === "issue.successful_run_handoff_escalated")).toBe(true); + }); + + it("escalates an exhausted successful handoff run that still leaves no disposition", async () => { + const { companyId, runId, issueId } = await seedStrandedIssueFixture({ + status: "in_progress", + runStatus: "succeeded", + livenessState: "advanced", + }); + const sourceRunId = randomUUID(); + await db + .update(heartbeatRuns) + .set({ + contextSnapshot: { + issueId, + taskId: issueId, + wakeReason: "finish_successful_run_handoff", + sourceRunId, + resumeFromRunId: sourceRunId, + handoffRequired: true, + handoffReason: "successful_run_missing_state", + missingDisposition: "clear_next_step", + handoffAttempt: 1, + maxHandoffAttempts: 1, + }, + }) + .where(eq(heartbeatRuns.id, runId)); + const heartbeat = heartbeatService(db); + + const result = await heartbeat.reconcileStrandedAssignedIssues(); + expect(result.continuationRequeued).toBe(0); + expect(result.successfulContinuationObserved).toBe(0); + expect(result.successfulRunHandoffEscalated).toBe(1); + + const recovery = await waitForValue(async () => + db.select().from(issues).where( + and( + eq(issues.companyId, companyId), + eq(issues.originKind, "stranded_issue_recovery"), + eq(issues.originId, issueId), + ), + ).then((rows) => rows[0] ?? null), + ); + expect(recovery?.description).toContain("Latest handoff run status: `succeeded`"); + expect(recovery?.description).toContain("Suggested"); + }); + it("clears the detached warning when the run reports activity again", async () => { const { runId } = await seedRunFixture({ includeIssue: false, @@ -1315,6 +1774,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { payload: expect.objectContaining({ issueId, mutation: "assigned_todo_liveness_dispatch", + modelProfile: "cheap", }), }); @@ -1326,6 +1786,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { taskId: issueId, wakeReason: "issue_assigned", source: "issue.assigned_todo_liveness_dispatch", + modelProfile: "cheap", }); expect((runs[0]?.contextSnapshot as Record)?.retryReason).toBeUndefined(); @@ -1433,6 +1894,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { payload: expect.objectContaining({ issueId: unblocked.issueId, mutation: "assigned_todo_liveness_dispatch", + modelProfile: "cheap", }), }); const unblockedRuns = await db @@ -1486,6 +1948,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { const retryRun = runs.find((row) => row.id !== runId); expect(retryRun?.id).toBeTruthy(); expect((retryRun?.contextSnapshot as Record)?.retryReason).toBe("assignment_recovery"); + expect(retryRun?.contextSnapshot).toMatchObject({ modelProfile: "cheap" }); if (retryRun) { await waitForRunToSettle(heartbeat, retryRun.id); } @@ -1524,6 +1987,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { retryReason: "issue_continuation_needed", retryOfRunId: runId, source: "issue.continuation_recovery", + modelProfile: "cheap", }); const recoveries = await db @@ -1575,6 +2039,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { const retryRun = runs.find((row) => row.id !== runId); expect((retryRun?.contextSnapshot as Record)?.retryReason).toBe("assignment_recovery"); + expect(retryRun?.contextSnapshot).toMatchObject({ modelProfile: "cheap" }); if (retryRun) { await waitForRunToSettle(heartbeat, retryRun.id); } @@ -1738,6 +2203,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { const retryRun = runs.find((row) => row.id !== runId); expect(retryRun?.id).toBeTruthy(); expect((retryRun?.contextSnapshot as Record)?.retryReason).toBe("issue_continuation_needed"); + expect(retryRun?.contextSnapshot).toMatchObject({ modelProfile: "cheap" }); if (retryRun) { await waitForRunToSettle(heartbeat, retryRun.id); } @@ -2215,6 +2681,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { retryReason: "issue_continuation_needed", retryOfRunId: runId, source: "issue.productive_terminal_continuation_recovery", + modelProfile: "cheap", }); const wakeups = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, agentId)); @@ -2281,6 +2748,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { retryReason: "issue_continuation_needed", retryOfRunId: runId, source: "issue.productive_terminal_continuation_recovery", + modelProfile: "cheap", }); }); @@ -2336,6 +2804,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { retryReason: "issue_continuation_needed", retryOfRunId: runId, source: "issue.productive_terminal_continuation_recovery", + modelProfile: "cheap", }); }); diff --git a/server/src/__tests__/heartbeat-retry-scheduling.test.ts b/server/src/__tests__/heartbeat-retry-scheduling.test.ts index 39b4d8987d9..e193dd3c69b 100644 --- a/server/src/__tests__/heartbeat-retry-scheduling.test.ts +++ b/server/src/__tests__/heartbeat-retry-scheduling.test.ts @@ -286,6 +286,7 @@ describeEmbeddedPostgres("heartbeat bounded retry scheduling", () => { retryOfRunId: sourceRunId, scheduledRetryAttempt: 1, scheduledRetryReason: "transient_failure", + contextSnapshot: expect.objectContaining({ modelProfile: "cheap" }), }); expect(retryRun?.scheduledRetryAt?.toISOString()).toBe(expectedDueAt.toISOString()); diff --git a/server/src/__tests__/issue-comment-reopen-routes.test.ts b/server/src/__tests__/issue-comment-reopen-routes.test.ts index 922cf658eb8..ebb68b94216 100644 --- a/server/src/__tests__/issue-comment-reopen-routes.test.ts +++ b/server/src/__tests__/issue-comment-reopen-routes.test.ts @@ -38,7 +38,12 @@ const mockTxInsert = vi.hoisted(() => vi.fn(() => ({ values: mockTxInsertValues const mockTx = vi.hoisted(() => ({ insert: mockTxInsert, })); +const mockDbSelectOrderBy = vi.hoisted(() => vi.fn(async () => [])); +const mockDbSelectWhere = vi.hoisted(() => vi.fn(() => ({ orderBy: mockDbSelectOrderBy }))); +const mockDbSelectFrom = vi.hoisted(() => vi.fn(() => ({ where: mockDbSelectWhere }))); +const mockDbSelect = vi.hoisted(() => vi.fn(() => ({ from: mockDbSelectFrom }))); const mockDb = vi.hoisted(() => ({ + select: mockDbSelect, transaction: vi.fn(async (fn: (tx: typeof mockTx) => Promise) => fn(mockTx)), })); const mockFeedbackService = vi.hoisted(() => ({ @@ -236,9 +241,17 @@ describe.sequential("issue comment reopen routes", () => { mockIssueTreeControlService.getActivePauseHoldGate.mockReset(); mockTxInsertValues.mockReset(); mockTxInsert.mockReset(); + mockDbSelect.mockReset(); + mockDbSelectFrom.mockReset(); + mockDbSelectWhere.mockReset(); + mockDbSelectOrderBy.mockReset(); mockDb.transaction.mockReset(); mockTxInsertValues.mockResolvedValue(undefined); mockTxInsert.mockImplementation(() => ({ values: mockTxInsertValues })); + mockDbSelectOrderBy.mockResolvedValue([]); + mockDbSelectWhere.mockImplementation(() => ({ orderBy: mockDbSelectOrderBy })); + mockDbSelectFrom.mockImplementation(() => ({ where: mockDbSelectWhere })); + mockDbSelect.mockImplementation(() => ({ from: mockDbSelectFrom })); mockDb.transaction.mockImplementation(async (fn: (tx: typeof mockTx) => Promise) => fn(mockTx)); mockHeartbeatService.wakeup.mockResolvedValue(undefined); mockHeartbeatService.reportRunActivity.mockResolvedValue(undefined); @@ -545,6 +558,68 @@ describe.sequential("issue comment reopen routes", () => { )); }); + it("passes validated comment presentation fields to trusted board comment writes", async () => { + const app = await installActor(createApp()); + mockIssueService.getById.mockResolvedValue(makeIssue("todo")); + mockIssueService.addComment.mockResolvedValue({ + id: "comment-1", + issueId: "11111111-1111-4111-8111-111111111111", + companyId: "company-1", + authorType: "user", + authorAgentId: null, + authorUserId: "local-board", + body: "Paperclip needs a disposition before this issue can continue.", + presentation: { kind: "system_notice", tone: "warning", detailsDefaultOpen: false }, + metadata: { + version: 1, + sections: [{ rows: [{ type: "key_value", label: "Cause", value: "successful_run_missing_state" }] }], + }, + createdAt: new Date(), + updatedAt: new Date(), + }); + mockIssueService.findMentionedAgents.mockResolvedValue([]); + + const metadata = { + version: 1, + sections: [{ rows: [{ type: "key_value", label: "Cause", value: "successful_run_missing_state" }] }], + }; + const presentation = { kind: "system_notice", tone: "warning" }; + const res = await request(app) + .post("/api/issues/11111111-1111-4111-8111-111111111111/comments") + .send({ + body: "Paperclip needs a disposition before this issue can continue.", + presentation, + metadata, + }); + + expect(res.status).toBe(201); + expect(mockIssueService.addComment).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + "Paperclip needs a disposition before this issue can continue.", + { agentId: undefined, userId: "local-board", runId: null }, + { + authorType: "user", + presentation: { kind: "system_notice", tone: "warning", detailsDefaultOpen: false }, + metadata, + }, + ); + }); + + it("rejects invalid comment metadata before writing a comment", async () => { + const app = await installActor(createApp()); + mockIssueService.getById.mockResolvedValue(makeIssue("todo")); + + const res = await request(app) + .post("/api/issues/11111111-1111-4111-8111-111111111111/comments") + .send({ + body: "Invalid metadata", + metadata: { version: 1, arbitrary: true }, + }); + + expect(res.status).toBe(400); + expect(mockIssueService.addComment).not.toHaveBeenCalled(); + }); + it("does not move dependency-blocked issues to todo via POST comments", async () => { mockIssueService.getById.mockResolvedValue(makeIssue("blocked")); mockIssueService.getDependencyReadiness.mockResolvedValue({ diff --git a/server/src/__tests__/issue-monitor-scheduler.test.ts b/server/src/__tests__/issue-monitor-scheduler.test.ts index 44fa27e0968..16fef429aa3 100644 --- a/server/src/__tests__/issue-monitor-scheduler.test.ts +++ b/server/src/__tests__/issue-monitor-scheduler.test.ts @@ -372,6 +372,7 @@ describeEmbeddedPostgres("issue monitor scheduler", () => { issueId, clearReason: "max_attempts_exhausted", maxAttempts: 1, + modelProfile: "cheap", }); const activity = await db @@ -414,6 +415,7 @@ describeEmbeddedPostgres("issue monitor scheduler", () => { expect(recoveryIssue).toMatchObject({ parentId: issueId, priority: "high", + assigneeAdapterOverrides: { modelProfile: "cheap" }, }); expect(["todo", "in_progress"]).toContain(recoveryIssue?.status); }); diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index 914006e5c0b..30ec5edcea4 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -91,6 +91,11 @@ const mockWorkProductService = vi.hoisted(() => ({ const mockEnvironmentService = vi.hoisted(() => ({})); +const mockDb = vi.hoisted(() => ({ + select: vi.fn(), + execute: vi.fn(), +})); + vi.mock("../services/index.js", () => ({ companyService: () => ({ getById: vi.fn(async () => ({ id: "company-1", attachmentMaxBytes: 10 * 1024 * 1024 })), @@ -130,7 +135,7 @@ function createApp() { }; next(); }); - app.use("/api", issueRoutes({} as any, {} as any)); + app.use("/api", issueRoutes(mockDb as any, {} as any)); app.use(errorHandler); return app; } @@ -186,6 +191,14 @@ describe.sequential("issue goal context routes", () => { mockDocumentsService.getIssueDocumentPayload.mockResolvedValue({}); mockDocumentsService.getIssueDocumentByKey.mockResolvedValue(null); mockExecutionWorkspaceService.getById.mockResolvedValue(null); + mockDb.select.mockReturnValue({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + orderBy: vi.fn(async () => []), + })), + })), + }); + mockDb.execute.mockResolvedValue([]); mockProjectService.getById.mockResolvedValue({ id: legacyProjectLinkedIssue.projectId, companyId: "company-1", diff --git a/server/src/__tests__/productivity-review-service.test.ts b/server/src/__tests__/productivity-review-service.test.ts index e2c61d20089..602267d44e5 100644 --- a/server/src/__tests__/productivity-review-service.test.ts +++ b/server/src/__tests__/productivity-review-service.test.ts @@ -201,6 +201,7 @@ describeEmbeddedPostgres("productivity review service", () => { expect(reviews).toHaveLength(1); expect(reviews[0]?.parentId).toBe(seeded.issueId); expect(reviews[0]?.assigneeAgentId).toBe(seeded.managerId); + expect(reviews[0]?.assigneeAdapterOverrides).toEqual({ modelProfile: "cheap" }); expect(reviews[0]?.originId).toBe(seeded.issueId); expect(reviews[0]?.originFingerprint).toBe(`productivity-review:${seeded.issueId}`); expect(reviews[0]?.description).toContain("Primary trigger: `no_comment_streak`"); diff --git a/server/src/__tests__/run-continuations.test.ts b/server/src/__tests__/run-continuations.test.ts index 82b132351d7..7daddf7bd8f 100644 --- a/server/src/__tests__/run-continuations.test.ts +++ b/server/src/__tests__/run-continuations.test.ts @@ -76,10 +76,12 @@ describe("run liveness continuations", () => { continuationAttempt: 1, maxContinuationAttempts: DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS, instruction: "Take the first concrete action now.", + modelProfile: "cheap", }); expect(decision.contextSnapshot).toMatchObject({ issueId, wakeReason: RUN_LIVENESS_CONTINUATION_REASON, + modelProfile: "cheap", livenessContinuationAttempt: 1, livenessContinuationMaxAttempts: DEFAULT_MAX_LIVENESS_CONTINUATION_ATTEMPTS, livenessContinuationSourceRunId: runId, diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index 8ae02c6df86..0cf1e664eeb 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -146,6 +146,7 @@ vi.mock("../services/index.js", () => ({ reconcileStrandedAssignedIssues: vi.fn(async () => ({ dispatchRequeued: 0, continuationRequeued: 0, + successfulRunHandoffEscalated: 0, escalated: 0, skipped: 0, issueIds: [], diff --git a/server/src/index.ts b/server/src/index.ts index bd7a9bc1510..105f23ecaf7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -686,6 +686,7 @@ export async function startServer(): Promise { reconciled.assignmentDispatched > 0 || reconciled.dispatchRequeued > 0 || reconciled.continuationRequeued > 0 || + reconciled.successfulRunHandoffEscalated > 0 || reconciled.escalated > 0 ) { logger.warn( @@ -751,6 +752,7 @@ export async function startServer(): Promise { reconciled.assignmentDispatched > 0 || reconciled.dispatchRequeued > 0 || reconciled.continuationRequeued > 0 || + reconciled.successfulRunHandoffEscalated > 0 || reconciled.escalated > 0 ) { logger.warn( diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 5887df79e0e..51e7e0ec11a 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -2,8 +2,9 @@ import { randomUUID } from "node:crypto"; import { Router, type Request, type Response } from "express"; import multer from "multer"; import { z } from "zod"; +import { and, desc, eq, inArray, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { issueExecutionDecisions } from "@paperclipai/db"; +import { activityLog, issueExecutionDecisions } from "@paperclipai/db"; import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, @@ -32,6 +33,7 @@ import { isClosedIsolatedExecutionWorkspace, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, type ExecutionWorkspace, + type SuccessfulRunHandoffState, } from "@paperclipai/shared"; import { trackAgentTaskCompleted } from "@paperclipai/shared/telemetry"; import { getTelemetryClient } from "../telemetry.js"; @@ -78,6 +80,7 @@ import { executionWorkspaceService as executionWorkspaceServiceDirect } from ".. import { feedbackService } from "../services/feedback.js"; import { instanceSettingsService } from "../services/instance-settings.js"; import { environmentService } from "../services/environments.js"; +import { redactSensitiveText } from "../redaction.js"; import { applyIssueExecutionPolicyTransition, normalizeIssueExecutionPolicy, @@ -113,6 +116,103 @@ type ExecutionStageWakeContext = { lastDecisionOutcome: ParsedExecutionState["lastDecisionOutcome"]; allowedActions: string[]; }; +type SuccessfulRunHandoffActivityRow = { + entityId: string; + action: string; + agentId: string | null; + runId: string | null; + details: Record | null; + createdAt: Date; +}; + +const SUCCESSFUL_RUN_HANDOFF_ACTIONS = [ + "issue.successful_run_handoff_required", + "issue.successful_run_handoff_resolved", + "issue.successful_run_handoff_escalated", +] as const; + +function readNonEmptyString(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function successfulRunHandoffStateFromActivity(row: { + action: string; + agentId: string | null; + runId: string | null; + details: Record | null; + createdAt: Date; +}): SuccessfulRunHandoffState | null { + const details = row.details ?? {}; + const state = + row.action === "issue.successful_run_handoff_required" + ? "required" + : row.action === "issue.successful_run_handoff_resolved" + ? "resolved" + : row.action === "issue.successful_run_handoff_escalated" + ? "escalated" + : null; + if (!state) return null; + + const detectedProgressSummary = + readNonEmptyString(details.detectedProgressSummary) + ?? readNonEmptyString(details.detected_progress_summary) + ?? null; + + return { + state, + required: state === "required", + sourceRunId: + readNonEmptyString(details.sourceRunId) + ?? readNonEmptyString(details.source_run_id) + ?? readNonEmptyString(details.resumeFromRunId) + ?? row.runId + ?? null, + correctiveRunId: + readNonEmptyString(details.correctiveRunId) + ?? readNonEmptyString(details.corrective_run_id) + ?? (state !== "required" ? row.runId : null), + assigneeAgentId: + readNonEmptyString(details.assigneeAgentId) + ?? readNonEmptyString(details.agentId) + ?? row.agentId + ?? null, + detectedProgressSummary: detectedProgressSummary + ? redactSensitiveText(detectedProgressSummary) + : null, + createdAt: row.createdAt, + }; +} + +async function listSuccessfulRunHandoffStates( + db: Db, + companyId: string, + issueIds: string[], +): Promise> { + if (issueIds.length === 0) return new Map(); + const result = await db.execute(sql` + SELECT DISTINCT ON (${activityLog.entityId}) + ${activityLog.entityId} AS "entityId", + ${activityLog.action} AS "action", + ${activityLog.agentId} AS "agentId", + ${activityLog.runId} AS "runId", + ${activityLog.details} AS "details", + ${activityLog.createdAt} AS "createdAt" + FROM ${activityLog} + WHERE ${activityLog.companyId} = ${companyId} + AND ${activityLog.entityType} = 'issue' + AND ${activityLog.entityId} IN (${sql.join(issueIds.map((id) => sql`${id}`), sql`, `)}) + AND ${activityLog.action} IN (${sql.join(SUCCESSFUL_RUN_HANDOFF_ACTIONS.map((action) => sql`${action}`), sql`, `)}) + ORDER BY ${activityLog.entityId}, ${activityLog.createdAt} DESC, ${activityLog.id} DESC + `); + const rows = Array.from(result as Iterable); + + const states = new Map(); + for (const row of rows) { + const state = successfulRunHandoffStateFromActivity(row); + if (state) states.set(row.entityId, state); + } + return states; +} function executionPrincipalsEqual( left: ParsedExecutionState["currentParticipant"] | null, @@ -1033,7 +1133,15 @@ export function issueRoutes( limit, offset, }); - res.json(result); + const handoffStates = await listSuccessfulRunHandoffStates( + db, + companyId, + result.map((issue) => issue.id), + ); + res.json(result.map((issue) => ({ + ...issue, + successfulRunHandoff: handoffStates.get(issue.id) ?? null, + }))); }); router.get("/companies/:companyId/labels", async (req, res) => { @@ -1221,6 +1329,7 @@ export function issueRoutes( blockerAttention, productivityReview, referenceSummary, + successfulRunHandoffStates, ] = await Promise.all([ resolveIssueProjectAndGoal(issue), svc.getAncestors(issue.id), @@ -1230,6 +1339,7 @@ export function issueRoutes( svc.listBlockerAttention(issue.companyId, [issue]).then((map) => map.get(issue.id) ?? null), svc.listProductivityReviews(issue.companyId, [issue.id]).then((map) => map.get(issue.id) ?? null), issueReferencesSvc.listIssueReferenceSummary(issue.id), + listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id]), ]); const mentionedProjects = mentionedProjectIds.length > 0 ? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds) @@ -1244,6 +1354,7 @@ export function issueRoutes( ancestors, ...(blockerAttention ? { blockerAttention } : {}), productivityReview, + successfulRunHandoff: successfulRunHandoffStates.get(issue.id) ?? null, blockedBy: relations.blockedBy, blocks: relations.blocks, relatedWork: referenceSummary, @@ -3688,6 +3799,10 @@ export function issueRoutes( agentId: actor.agentId ?? undefined, userId: actor.actorType === "user" ? actor.actorId : undefined, runId: actor.runId, + }, { + authorType: req.body.authorType ?? (actor.actorType === "agent" ? "agent" : "user"), + presentation: req.body.presentation ?? null, + metadata: req.body.metadata ?? null, }); await issueReferencesSvc.syncComment(comment.id); const commentReferenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(currentIssue.id); diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 9cd1946d8a4..d511a8861e4 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -16,6 +16,7 @@ import type { CompanyPortabilityImportResult, CompanyPortabilityInclude, CompanyPortabilityManifest, + CompanyPortabilityIssueCommentManifestEntry, CompanyPortabilityPreview, CompanyPortabilityPreviewAgentPlan, CompanyPortabilityPreviewResult, @@ -42,6 +43,9 @@ import { ROUTINE_TRIGGER_SIGNING_MODES, deriveProjectUrlKey, envConfigSchema, + issueCommentAuthorTypeSchema, + issueCommentMetadataSchema, + issueCommentPresentationSchema, normalizeAgentUrlKey, } from "@paperclipai/shared"; import { @@ -644,6 +648,96 @@ function asInteger(value: unknown): number | null { return typeof value === "number" && Number.isInteger(value) ? value : null; } +function hasOwn(record: Record, key: string) { + return Object.prototype.hasOwnProperty.call(record, key); +} + +function readStringArray(value: unknown): string[] | null { + if (!Array.isArray(value)) return null; + const entries = value.filter((entry): entry is string => typeof entry === "string"); + return entries.length === value.length ? entries : null; +} + +function derivePortableCommentAuthorType(value: Record) { + const explicit = issueCommentAuthorTypeSchema.safeParse(value.authorType); + if (explicit.success) return explicit.data; + return asString(value.authorAgentSlug) ? "agent" : asString(value.authorUserId) ? "user" : "system"; +} + +function readPortableIssueComments( + value: unknown, + warnings: string[], + sourceLabel: string, +): CompanyPortabilityIssueCommentManifestEntry[] { + if (value === undefined || value === null) return []; + if (!Array.isArray(value)) { + warnings.push(`${sourceLabel} comments were ignored because they are not an array.`); + return []; + } + + const comments: CompanyPortabilityIssueCommentManifestEntry[] = []; + for (const [index, entry] of value.entries()) { + if (!isPlainRecord(entry)) { + warnings.push(`${sourceLabel} comment ${index + 1} was ignored because it is not an object.`); + continue; + } + const body = asString(entry.body); + if (!body) { + warnings.push(`${sourceLabel} comment ${index + 1} was ignored because it has no body.`); + continue; + } + const presentation = entry.presentation == null ? null : issueCommentPresentationSchema.safeParse(entry.presentation); + if (presentation && !presentation.success) { + warnings.push(`${sourceLabel} comment ${index + 1} has invalid presentation metadata and was ignored.`); + continue; + } + const metadata = entry.metadata == null ? null : issueCommentMetadataSchema.safeParse(entry.metadata); + if (metadata && !metadata.success) { + warnings.push(`${sourceLabel} comment ${index + 1} has invalid hidden metadata and was ignored.`); + continue; + } + const createdAt = asString(entry.createdAt); + comments.push({ + body, + authorType: derivePortableCommentAuthorType(entry), + authorAgentSlug: asString(entry.authorAgentSlug), + authorUserId: asString(entry.authorUserId), + presentation: presentation ? presentation.data : null, + metadata: metadata ? metadata.data : null, + createdAt: createdAt && Number.isNaN(Date.parse(createdAt)) ? null : createdAt, + }); + } + return comments; +} + +function appendCodexImportArg(adapterConfig: Record, arg: string) { + const extraArgs = readStringArray(adapterConfig.extraArgs); + if (extraArgs) { + if (!extraArgs.includes(arg)) adapterConfig.extraArgs = [...extraArgs, arg]; + return; + } + + const legacyArgs = readStringArray(adapterConfig.args); + if (legacyArgs && legacyArgs.length > 0) { + if (!legacyArgs.includes(arg)) adapterConfig.args = [...legacyArgs, arg]; + return; + } + + if (legacyArgs?.includes(arg)) return; + adapterConfig.extraArgs = [arg]; +} + +function applyImportAdapterRunDefaults( + adapterType: string, + adapterConfig: Record, +) { + const next = { ...adapterConfig }; + if (adapterType === "codex_local") { + appendCodexImportArg(next, "--skip-git-repo-check"); + } + return next; +} + function normalizeRoutineTriggerExtension(value: unknown): CompanyPortabilityIssueRoutineTriggerManifestEntry | null { if (!isPlainRecord(value)) return null; const kind = asString(value.kind); @@ -2685,6 +2779,7 @@ function buildManifestFromPackageFiles( assigneeAdapterOverrides: isPlainRecord(extension.assigneeAdapterOverrides) ? extension.assigneeAdapterOverrides : null, + comments: readPortableIssueComments(extension.comments, warnings, `Task ${slug}`), metadata: isPlainRecord(extension.metadata) ? extension.metadata : null, }); if (frontmatter.kind && frontmatter.kind !== "task") { @@ -2804,7 +2899,10 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { if (mode === "agent_safe" && IMPORT_FORBIDDEN_ADAPTER_TYPES.has(effectiveAdapterType)) { throw forbidden(`Adapter type "${effectiveAdapterType}" is not allowed in safe imports`); } - const nextAdapterConfig = writePaperclipSkillSyncPreference({ ...adapterConfig }, desiredSkills); + const nextAdapterConfig = writePaperclipSkillSyncPreference( + applyImportAdapterRunDefaults(effectiveAdapterType, adapterConfig), + desiredSkills, + ); delete nextAdapterConfig.promptTemplate; delete nextAdapterConfig.bootstrapPromptTemplate; delete nextAdapterConfig.instructionsFilePath; @@ -3380,6 +3478,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { }); } } + const comments = await issuesSvc.listComments(issue.id, { order: "asc" }); files[taskPath] = buildMarkdown( { name: issue.title, @@ -3397,6 +3496,20 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { projectWorkspaceKey: projectWorkspaceKey ?? undefined, executionWorkspaceSettings: issue.executionWorkspaceSettings ?? undefined, assigneeAdapterOverrides: issue.assigneeAdapterOverrides ?? undefined, + comments: comments.length > 0 + ? comments.map((comment) => ({ + body: comment.body, + authorType: comment.authorType, + authorAgentSlug: comment.authorAgentId ? (idToSlug.get(comment.authorAgentId) ?? null) : null, + // Portable bundles preserve author kind, but not raw board user ids. + authorUserId: null, + presentation: comment.presentation, + metadata: comment.metadata, + createdAt: comment.createdAt instanceof Date + ? comment.createdAt.toISOString() + : new Date(comment.createdAt).toISOString(), + })) + : undefined, }); paperclipTasksOut[taskSlug] = isPlainRecord(extension) ? extension : {}; } @@ -4496,7 +4609,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { warnings.push(`Task ${manifestIssue.slug} was downgraded to todo because its assignee could not be imported as assignable work.`); issueStatus = "todo"; } - await issues.create(targetCompany.id, { + const createdIssue = await issues.create(targetCompany.id, { projectId, projectWorkspaceId, title: manifestIssue.title, @@ -4511,6 +4624,30 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { executionWorkspaceSettings: manifestIssue.executionWorkspaceSettings, labelIds: manifestIssue.labelIds ?? [], }); + for (const comment of manifestIssue.comments ?? []) { + const authorAgentId = comment.authorType === "agent" && comment.authorAgentSlug + ? importedSlugToAgentId.get(comment.authorAgentSlug) + ?? existingSlugToAgentId.get(comment.authorAgentSlug) + ?? null + : null; + if (comment.authorType === "agent" && comment.authorAgentSlug && !authorAgentId) { + warnings.push(`Comment on task ${manifestIssue.slug} was imported as a system comment because author agent ${comment.authorAgentSlug} was not imported.`); + } + const authorType = authorAgentId + ? "agent" + : comment.authorType === "user" + ? "user" + : "system"; + await issues.addComment(createdIssue.id, comment.body, { + agentId: authorAgentId ?? undefined, + userId: authorType === "user" ? actorUserId ?? undefined : undefined, + }, { + authorType, + presentation: comment.presentation, + metadata: comment.metadata, + createdAt: comment.createdAt, + }); + } } } diff --git a/server/src/services/feedback.ts b/server/src/services/feedback.ts index 26ce6107fd6..4df0ca625b4 100644 --- a/server/src/services/feedback.ts +++ b/server/src/services/feedback.ts @@ -89,6 +89,9 @@ type FeedbackTargetRecord = { createdAt: Date; authorAgentId: string | null; authorUserId: string | null; + authorType?: string | null; + presentation?: unknown; + metadata?: unknown; createdByRunId: string | null; documentId: string | null; documentKey: string | null; @@ -797,6 +800,9 @@ async function resolveFeedbackTarget( companyId: issueComments.companyId, authorAgentId: issueComments.authorAgentId, authorUserId: issueComments.authorUserId, + authorType: issueComments.authorType, + presentation: issueComments.presentation, + metadata: issueComments.metadata, createdByRunId: issueComments.createdByRunId, body: issueComments.body, createdAt: issueComments.createdAt, @@ -820,6 +826,9 @@ async function resolveFeedbackTarget( createdAt: targetComment.createdAt, authorAgentId: targetComment.authorAgentId, authorUserId: targetComment.authorUserId, + authorType: targetComment.authorType ?? (targetComment.authorAgentId ? "agent" : targetComment.authorUserId ? "user" : "system"), + presentation: targetComment.presentation ?? null, + metadata: targetComment.metadata ?? null, createdByRunId: targetComment.createdByRunId ?? null, documentId: null, documentKey: null, @@ -833,6 +842,9 @@ async function resolveFeedbackTarget( createdAt: targetComment.createdAt.toISOString(), authorAgentId: targetComment.authorAgentId, authorUserId: targetComment.authorUserId, + authorType: targetComment.authorType ?? (targetComment.authorAgentId ? "agent" : targetComment.authorUserId ? "user" : "system"), + presentation: targetComment.presentation ?? null, + metadata: targetComment.metadata ?? null, createdByRunId: targetComment.createdByRunId ?? null, issuePath, targetPath: issuePath ? `${issuePath}#comment-${targetComment.id}` : null, @@ -918,6 +930,9 @@ async function listIssueContextItems( createdAt: issueComments.createdAt, authorAgentId: issueComments.authorAgentId, authorUserId: issueComments.authorUserId, + authorType: issueComments.authorType, + presentation: issueComments.presentation, + metadata: issueComments.metadata, createdByRunId: issueComments.createdByRunId, }) .from(issueComments) @@ -952,6 +967,9 @@ async function listIssueContextItems( createdAt: row.createdAt, authorAgentId: row.authorAgentId, authorUserId: row.authorUserId, + authorType: row.authorType ?? (row.authorAgentId ? "agent" : row.authorUserId ? "user" : "system"), + presentation: row.presentation ?? null, + metadata: row.metadata ?? null, createdByRunId: row.createdByRunId ?? null, documentId: null, documentKey: null, @@ -1023,6 +1041,9 @@ async function buildIssueContext( createdAt: item.createdAt.toISOString(), authorAgentId: item.authorAgentId, authorUserId: item.authorUserId, + authorType: item.authorType ?? null, + presentation: item.presentation ?? null, + metadata: item.metadata ?? null, createdByRunId: item.createdByRunId, documentKey: item.documentKey, documentTitle: item.documentTitle, diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 49a3a8e2d9d..32c046896d1 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -26,13 +26,16 @@ import { agentTaskSessions, agentWakeupRequests, activityLog, + approvals, companySkills as companySkillsTable, documentRevisions, issueDocuments, heartbeatRunEvents, heartbeatRuns, + issueApprovals, issueComments, issueRelations, + issueThreadInteractions, issues, issueWorkProducts, projects, @@ -119,18 +122,33 @@ import { import { instanceSettingsService } from "./instance-settings.js"; import { RECOVERY_ORIGIN_KINDS, + FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + SUCCESSFUL_RUN_MISSING_STATE_REASON, RUN_LIVENESS_CONTINUATION_REASON, buildRunLivenessContinuationIdempotencyKey, + buildFinishSuccessfulRunHandoffIdempotencyKey, + buildSuccessfulRunHandoffRequiredNotice, decideRunLivenessContinuation, + decideSuccessfulRunHandoff, + findExistingFinishSuccessfulRunHandoffWake, findExistingRunLivenessContinuationWake, + SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY, readContinuationAttempt, } from "./recovery/index.js"; import { isAutomaticRecoverySuppressedByPauseHold } from "./recovery/pause-hold-guard.js"; +import { + recoveryAssigneeAdapterOverrides, + withRecoveryModelProfileHint, +} from "./recovery/model-profile-hint.js"; import { recoveryService } from "./recovery/service.js"; import { productivityReviewService } from "./productivity-review.js"; import { withAgentStartLock } from "./agent-start-lock.js"; -import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js"; -import { redactEventPayload } from "../redaction.js"; +import { + redactCurrentUserText, + redactCurrentUserValue, + type CurrentUserRedactionOptions, +} from "../log-redaction.js"; +import { redactEventPayload, redactSensitiveText } from "../redaction.js"; import { hasSessionCompactionThresholds, resolveSessionCompactionPolicy, @@ -150,6 +168,16 @@ const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024; const MAX_PERSISTED_LOG_CHUNK_CHARS = 64 * 1024; const MAX_RUN_EVENT_PAYLOAD_STRING_CHARS = 16 * 1024; const MAX_RUN_EVENT_PAYLOAD_ARRAY_ITEMS = 50; + +export function redactDetectedSuccessfulRunProgressSummaryForBoard( + summary: string, + currentUserRedactionOptions?: CurrentUserRedactionOptions, +) { + const normalized = summary.replace(/\s+/g, " ").trim(); + const redacted = redactSensitiveText(redactCurrentUserText(normalized, currentUserRedactionOptions)); + return redacted.length <= 280 ? redacted : `${redacted.slice(0, 277)}...`; +} + const MAX_RUN_EVENT_PAYLOAD_OBJECT_KEYS = 100; const MAX_RUN_EVENT_PAYLOAD_DEPTH = 6; const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = AGENT_DEFAULT_MAX_CONCURRENT_RUNS; @@ -1837,8 +1865,11 @@ async function buildPaperclipWakePayload(input: { id: issueComments.id, issueId: issueComments.issueId, body: issueComments.body, + authorType: issueComments.authorType, authorAgentId: issueComments.authorAgentId, authorUserId: issueComments.authorUserId, + presentation: issueComments.presentation, + metadata: issueComments.metadata, createdAt: issueComments.createdAt, }) .from(issueComments) @@ -1882,8 +1913,11 @@ async function buildPaperclipWakePayload(input: { comments.push({ id: row.id, issueId: row.issueId, + authorType: row.authorType ?? (row.authorAgentId ? "agent" : row.authorUserId ? "user" : "system"), body, bodyTruncated, + presentation: row.presentation ?? null, + metadata: row.metadata ?? null, createdAt: row.createdAt.toISOString(), author: row.authorAgentId ? { type: "agent", id: row.authorAgentId } @@ -2541,6 +2575,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) projectId: input.claimed.projectId, goalId: input.claimed.goalId, assigneeAgentId: input.claimed.assigneeAgentId, + assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides(), originKind: RECOVERY_ORIGIN_KINDS.strandedIssueRecovery, originId: input.claimed.id, originFingerprint: `issue_monitor:${input.clearReason}`, @@ -2554,15 +2589,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", reason: "issue_monitor_recovery_issue", idempotencyKey: `issue-monitor-recovery-issue:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, - payload: { issueId: recoveryIssue.id, sourceIssueId: input.claimed.id }, + payload: withRecoveryModelProfileHint({ issueId: recoveryIssue.id, sourceIssueId: input.claimed.id }), requestedByActorType: input.actorType, requestedByActorId: input.actorId, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: recoveryIssue.id, sourceIssueId: input.claimed.id, source: "issue.monitor.recovery_issue", wakeReason: "issue_monitor_recovery_issue", - }, + }), }); } @@ -2615,7 +2650,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", reason: "issue_monitor_recovery", idempotencyKey: `issue-monitor-recovery:${input.claimed.id}:${input.clearReason}:${input.scheduledAtIso}`, - payload: { + payload: withRecoveryModelProfileHint({ issueId: input.claimed.id, monitorAttemptCount: input.nextAttemptCount, monitorNotes: input.claimed.monitorNotes ?? null, @@ -2623,10 +2658,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) serviceName: input.monitor?.serviceName ?? null, timeoutAt: input.monitor?.timeoutAt ?? null, maxAttempts: input.monitor?.maxAttempts ?? null, - }, + }), requestedByActorType: input.actorType, requestedByActorId: input.actorId, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: input.claimed.id, source: "issue.monitor.recovery", wakeReason: "issue_monitor_recovery", @@ -2636,7 +2671,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) serviceName: input.monitor?.serviceName ?? null, timeoutAt: input.monitor?.timeoutAt ?? null, maxAttempts: input.monitor?.maxAttempts ?? null, - }, + }), }); await logActivity(db, { @@ -3817,6 +3852,287 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } } + function issueUiLink(issue: Pick) { + const label = issue.identifier ?? issue.id; + const prefix = issue.identifier?.split("-")[0] || "PAP"; + return `[${label}](/${prefix}/issues/${label})`; + } + + async function buildDetectedSuccessfulRunProgressSummary(run: typeof heartbeatRuns.$inferSelect) { + const resultJson = parseObject(run.resultJson); + const candidates = [ + readNonEmptyString(run.nextAction) ? `Next action noted: ${readNonEmptyString(run.nextAction)}` : null, + readNonEmptyString(run.livenessReason), + readNonEmptyString(resultJson.summary), + readNonEmptyString(resultJson.result), + readNonEmptyString(resultJson.message), + ].filter((value): value is string => Boolean(value)); + const summary = candidates[0]; + if (!summary) return null; + return redactDetectedSuccessfulRunProgressSummaryForBoard( + summary, + await getCurrentUserRedactionOptions(), + ); + } + + async function addSuccessfulRunHandoffCommentOnce(input: { + issue: Pick; + run: typeof heartbeatRuns.$inferSelect; + agent: Pick; + detectedProgressSummary: string; + }) { + const existing = await db + .select({ id: issueComments.id }) + .from(issueComments) + .where( + and( + eq(issueComments.companyId, input.run.companyId), + eq(issueComments.issueId, input.issue.id), + eq(issueComments.createdByRunId, input.run.id), + sql`(${issueComments.body} = ${SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY} or ${issueComments.body} like '## This issue still needs a next step%' or ${issueComments.body} like '## Successful run missing issue disposition%')`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null); + if (existing) return null; + const notice = buildSuccessfulRunHandoffRequiredNotice(input); + return issuesSvc.addComment( + input.issue.id, + notice.body, + { runId: input.run.id }, + { + authorType: "system", + presentation: notice.presentation, + metadata: notice.metadata, + }, + ); + } + + async function handleSuccessfulRunHandoff(run: typeof heartbeatRuns.$inferSelect, agent: typeof agents.$inferSelect) { + if (run.status !== "succeeded") return; + const context = parseObject(run.contextSnapshot); + const issueId = readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); + if (!issueId) return; + + const issue = await db + .select({ + id: issues.id, + companyId: issues.companyId, + identifier: issues.identifier, + title: issues.title, + status: issues.status, + assigneeAgentId: issues.assigneeAgentId, + assigneeUserId: issues.assigneeUserId, + executionState: issues.executionState, + projectId: issues.projectId, + }) + .from(issues) + .where(and(eq(issues.id, issueId), eq(issues.companyId, run.companyId))) + .then((rows) => rows[0] ?? null); + const idempotencyKey = issue + ? buildFinishSuccessfulRunHandoffIdempotencyKey({ + issueId: issue.id, + sourceRunId: run.id, + }) + : null; + const taskKey = deriveTaskKeyWithHeartbeatFallback(context, null); + const detectedProgressSummary = await buildDetectedSuccessfulRunProgressSummary(run); + + const [ + activeExecutionPath, + queuedWake, + pendingInteraction, + pendingApproval, + explicitBlocker, + openRecoveryIssue, + existingWake, + budgetBlock, + pauseHold, + ] = await Promise.all([ + issue + ? db + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where( + and( + eq(heartbeatRuns.companyId, issue.companyId), + eq(heartbeatRuns.agentId, run.agentId), + inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]), + sql`( + ${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id} + or ${heartbeatRuns.contextSnapshot} ->> 'taskId' = ${issue.id} + )`, + sql`${heartbeatRuns.id} <> ${run.id}`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + issue + ? db + .select({ id: agentWakeupRequests.id }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, issue.companyId), + eq(agentWakeupRequests.agentId, run.agentId), + inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution", "claimed"]), + sql`( + ${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id} + or ${agentWakeupRequests.payload} ->> 'taskId' = ${issue.id} + or ${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'issueId' = ${issue.id} + or ${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'taskId' = ${issue.id} + )`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + issue + ? db + .select({ id: issueThreadInteractions.id }) + .from(issueThreadInteractions) + .where( + and( + eq(issueThreadInteractions.companyId, issue.companyId), + eq(issueThreadInteractions.issueId, issue.id), + eq(issueThreadInteractions.status, "pending"), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + issue + ? db + .select({ id: issueApprovals.approvalId }) + .from(issueApprovals) + .innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id)) + .where( + and( + eq(issueApprovals.companyId, issue.companyId), + eq(issueApprovals.issueId, issue.id), + inArray(approvals.status, ["pending", "revision_requested"]), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + issue + ? db + .select({ id: issueRelations.issueId }) + .from(issueRelations) + .where( + and( + eq(issueRelations.companyId, issue.companyId), + eq(issueRelations.relatedIssueId, issue.id), + eq(issueRelations.type, "blocks"), + sql`exists ( + select 1 + from issues blocker + where blocker.id = ${issueRelations.issueId} + and blocker.company_id = ${issue.companyId} + and blocker.status not in ('done', 'cancelled') + and blocker.hidden_at is null + )`, + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + issue + ? db + .select({ id: issues.id }) + .from(issues) + .where( + and( + eq(issues.companyId, issue.companyId), + inArray(issues.originKind, [ + RECOVERY_ORIGIN_KINDS.strandedIssueRecovery, + RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation, + ]), + eq(issues.originId, issue.id), + isNull(issues.hiddenAt), + notInArray(issues.status, ["done", "cancelled"]), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null) + : Promise.resolve(null), + idempotencyKey + ? findExistingFinishSuccessfulRunHandoffWake(db, { + companyId: run.companyId, + idempotencyKey, + }) + : Promise.resolve(null), + issue + ? budgets.getInvocationBlock(issue.companyId, run.agentId, { + issueId: issue.id, + projectId: issue.projectId, + }) + : Promise.resolve(null), + issue + ? treeControlSvc.getActivePauseHoldGate(issue.companyId, issue.id) + : Promise.resolve(null), + ]); + + const decision = decideSuccessfulRunHandoff({ + run, + issue, + agent, + livenessState: run.livenessState as RunLivenessState | null, + detectedProgressSummary, + taskKey, + hasActiveExecutionPath: Boolean(activeExecutionPath), + hasQueuedWake: Boolean(queuedWake), + hasPendingInteractionOrApproval: Boolean(pendingInteraction || pendingApproval), + hasExplicitBlockerPath: Boolean(explicitBlocker), + hasOpenRecoveryIssue: Boolean(openRecoveryIssue), + hasPauseHold: Boolean(pauseHold), + budgetBlocked: Boolean(budgetBlock), + idempotentWakeExists: Boolean(existingWake), + }); + + if (decision.kind !== "enqueue" || !issue) return; + + const handoffRun = await enqueueWakeup(run.agentId, { + source: "automation", + triggerDetail: "system", + reason: FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + payload: decision.payload, + contextSnapshot: decision.contextSnapshot, + idempotencyKey: decision.idempotencyKey, + requestedByActorType: "system", + requestedByActorId: "heartbeat", + }); + if (!handoffRun) return; + + await addSuccessfulRunHandoffCommentOnce({ + issue, + run, + agent, + detectedProgressSummary: detectedProgressSummary ?? "The run reported progress, but did not choose a next step.", + }); + await logActivity(db, { + companyId: issue.companyId, + actorType: "system", + actorId: "heartbeat", + agentId: run.agentId, + runId: run.id, + action: "issue.successful_run_handoff_required", + entityType: "issue", + entityId: issue.id, + details: { + label: "Successful run missing issue disposition", + sourceRunId: run.id, + correctiveRunId: handoffRun.id, + handoffReason: SUCCESSFUL_RUN_MISSING_STATE_REASON, + missingDisposition: "clear_next_step", + detectedProgressSummary, + issue: issueUiLink(issue), + }, + }); + } + async function appendRunEvent( run: typeof heartbeatRuns.$inferSelect, seq: number, @@ -3998,13 +4314,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const contextSnapshot = parseObject(run.contextSnapshot); const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const retryContextSnapshot = { + const retryContextSnapshot = withRecoveryModelProfileHint({ ...contextSnapshot, retryOfRunId: run.id, wakeReason: "missing_issue_comment", retryReason: "missing_issue_comment", missingIssueCommentForRunId: run.id, - }; + }); const now = new Date(); const retryRun = await db.transaction(async (tx) => { @@ -4027,11 +4343,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: "missing_issue_comment", - payload: { + payload: withRecoveryModelProfileHint({ issueId, retryOfRunId: run.id, retryReason: "missing_issue_comment", - }, + }), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -4219,12 +4535,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const issueId = readNonEmptyString(contextSnapshot.issueId); const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const retryContextSnapshot = { + const retryContextSnapshot = withRecoveryModelProfileHint({ ...contextSnapshot, retryOfRunId: run.id, wakeReason: "process_lost_retry", retryReason: "process_lost", - }; + }); const queued = await db.transaction(async (tx) => { const wakeupRequest = await tx @@ -4235,10 +4551,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: "process_lost_retry", - payload: { + payload: withRecoveryModelProfileHint({ ...(issueId ? { issueId } : {}), retryOfRunId: run.id, - }, + }), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -4675,7 +4991,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } const taskKey = deriveTaskKeyWithHeartbeatFallback(contextSnapshot, null); const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey); - const retryContextSnapshot: Record = { + const retryContextSnapshot: Record = withRecoveryModelProfileHint({ ...contextSnapshot, retryOfRunId: run.id, wakeReason, @@ -4685,7 +5001,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) scheduledRetryAt: schedule.dueAt.toISOString(), ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}), ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), - }; + }); const maxTurnContinuationIdempotencyKey = retryReason === MAX_TURN_CONTINUATION_RETRY_REASON ? `max-turn-continuation:${run.companyId}:${issueId ?? "no-issue"}:${run.id}:${schedule.attempt}` : null; @@ -4846,7 +5162,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: wakeReason, - payload: { + payload: withRecoveryModelProfileHint({ ...(issueId ? { issueId } : {}), retryOfRunId: run.id, retryReason, @@ -4855,7 +5171,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) scheduledRetryAt: schedule.dueAt.toISOString(), ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}), ...(codexTransientFallbackMode ? { codexTransientFallbackMode } : {}), - }, + }), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -6171,6 +6487,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) .select({ id: issueComments.id, body: issueComments.body, + authorType: issueComments.authorType, + authorAgentId: issueComments.authorAgentId, + authorUserId: issueComments.authorUserId, + presentation: issueComments.presentation, + metadata: issueComments.metadata, }) .from(issueComments) .where(and( @@ -7235,9 +7556,18 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } else if (outcome === "failed" && readTransientRecoveryContractFromRun(livenessRun)) { await scheduleBoundedRetryForRun(livenessRun, agent); } - await finalizeIssueCommentPolicy(livenessRun, agent); + const issueCommentPolicyResult = await finalizeIssueCommentPolicy(livenessRun, agent); await releaseIssueExecutionAndPromote(livenessRun); await handleRunLivenessContinuation(livenessRun); + await handleSuccessfulRunHandoff( + issueCommentPolicyResult.outcome === "retry_queued" || issueCommentPolicyResult.outcome === "retry_exhausted" + ? { + ...livenessRun, + issueCommentStatus: issueCommentPolicyResult.outcome, + } + : livenessRun, + agent, + ); } if (finalizedRun) { @@ -7739,10 +8069,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) source: "automation", triggerDetail: "system", reason: recoveryReason, - payload: { + payload: withRecoveryModelProfileHint({ issueId: issue.id, retryOfRunId: run.id, - }, + }), status: "queued", requestedByActorType: "system", requestedByActorId: null, @@ -7760,14 +8090,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) triggerDetail: "system", status: "queued", wakeupRequestId: wakeupRequest.id, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: issue.id, taskId: issue.id, wakeReason: recoveryReason, retryReason, source: recoverySource, retryOfRunId: run.id, - }, + }), sessionIdBefore: recoverySessionBefore, retryOfRunId: run.id, updatedAt: now, diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index adb49b33ad6..59cb3e8c78b 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -28,6 +28,9 @@ import { projects, } from "@paperclipai/db"; import type { + IssueCommentAuthorType, + IssueCommentMetadata, + IssueCommentPresentation, IssueBlockerAttention, IssueProductivityReview, IssueProductivityReviewTrigger, @@ -37,6 +40,9 @@ import { clampIssueRequestDepth, extractAgentMentionIds, extractProjectMentionIds, + issueCommentAuthorTypeSchema, + issueCommentMetadataSchema, + issueCommentPresentationSchema, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@paperclipai/shared"; @@ -1679,10 +1685,47 @@ export function issueService(db: Db) { return enriched; } - function redactIssueComment(comment: T, censorUsernameInLogs: boolean): T { + function deriveIssueCommentAuthorType(comment: { + authorType?: string | null; + authorAgentId?: string | null; + authorUserId?: string | null; + }): IssueCommentAuthorType { + const explicit = issueCommentAuthorTypeSchema.safeParse(comment.authorType); + if (explicit.success) return explicit.data; + if (comment.authorAgentId) return "agent"; + if (comment.authorUserId) return "user"; + return "system"; + } + + function assertIssueCommentAuthorTypeAllowed( + actor: { agentId?: string | null; userId?: string | null }, + authorType: IssueCommentAuthorType, + ) { + if (actor.agentId && authorType !== "agent") { + throw unprocessable("Comment authorType must match authenticated actor"); + } + if (actor.userId && authorType !== "user") { + throw unprocessable("Comment authorType must match authenticated actor"); + } + if (!actor.agentId && !actor.userId && authorType !== "system") { + throw unprocessable("System comments cannot use user or agent authorType without an author id"); + } + } + + function redactIssueComment( + comment: T, + censorUsernameInLogs: boolean, + ): T & { + authorType: IssueCommentAuthorType; + presentation: IssueCommentPresentation | null; + metadata: IssueCommentMetadata | null; + } { return { ...comment, + authorType: deriveIssueCommentAuthorType(comment), body: redactCurrentUserText(comment.body, { enabled: censorUsernameInLogs }), + presentation: issueCommentPresentationSchema.nullable().catch(null).parse(comment.presentation ?? null), + metadata: issueCommentMetadataSchema.nullable().catch(null).parse(comment.metadata ?? null), }; } @@ -3743,6 +3786,12 @@ export function issueService(db: Db) { issueId: string, body: string, actor: { agentId?: string; userId?: string; runId?: string | null }, + options?: { + authorType?: IssueCommentAuthorType | null; + presentation?: IssueCommentPresentation | null; + metadata?: IssueCommentMetadata | null; + createdAt?: Date | string | null; + }, ) => { const issue = await db .select({ companyId: issues.companyId }) @@ -3756,6 +3805,13 @@ export function issueService(db: Db) { enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, }; const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions); + const authorType = issueCommentAuthorTypeSchema.parse( + options?.authorType ?? (actor.agentId ? "agent" : actor.userId ? "user" : "system"), + ); + assertIssueCommentAuthorTypeAllowed(actor, authorType); + const presentation = issueCommentPresentationSchema.nullable().parse(options?.presentation ?? null); + const metadata = issueCommentMetadataSchema.nullable().parse(options?.metadata ?? null); + const createdAt = options?.createdAt ? new Date(options.createdAt) : null; const [comment] = await db .insert(issueComments) .values({ @@ -3763,8 +3819,12 @@ export function issueService(db: Db) { issueId, authorAgentId: actor.agentId ?? null, authorUserId: actor.userId ?? null, + authorType, createdByRunId: actor.runId ?? null, body: redactedBody, + presentation, + metadata, + ...(createdAt && !Number.isNaN(createdAt.getTime()) ? { createdAt } : {}), }) .returning(); diff --git a/server/src/services/productivity-review.ts b/server/src/services/productivity-review.ts index 1b14a1cbbee..c9cd0dcd514 100644 --- a/server/src/services/productivity-review.ts +++ b/server/src/services/productivity-review.ts @@ -14,6 +14,10 @@ import { logger } from "../middleware/logger.js"; import { logActivity } from "./activity-log.js"; import { budgetService } from "./budgets.js"; import { issueService } from "./issues.js"; +import { + recoveryAssigneeAdapterOverrides, + withRecoveryModelProfileHint, +} from "./recovery/model-profile-hint.js"; import { RECOVERY_ORIGIN_KINDS } from "./recovery/origins.js"; export const PRODUCTIVITY_REVIEW_ORIGIN_KIND = RECOVERY_ORIGIN_KINDS.issueProductivityReview; @@ -687,6 +691,7 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque goalId: evidence.sourceIssue.goalId, billingCode: evidence.sourceIssue.billingCode, assigneeAgentId: ownerAgentId, + assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides(), originKind: PRODUCTIVITY_REVIEW_ORIGIN_KIND, originId: evidence.sourceIssue.id, originFingerprint: productivityReviewFingerprint(evidence.sourceIssue.id), @@ -732,21 +737,21 @@ export function productivityReviewService(db: Db, deps?: { enqueueWakeup?: Enque source: "assignment", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: review.id, sourceIssueId: evidence.sourceIssue.id, trigger: evidence.trigger, - }, + }), requestedByActorType: "system", requestedByActorId: "productivity_review", - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: review.id, taskId: review.id, wakeReason: "issue_assigned", source: PRODUCTIVITY_REVIEW_ORIGIN_KIND, sourceIssueId: evidence.sourceIssue.id, productivityReviewTrigger: evidence.trigger, - }, + }), }); } diff --git a/server/src/services/recovery/index.ts b/server/src/services/recovery/index.ts index 6844a238b3e..3262b9e7467 100644 --- a/server/src/services/recovery/index.ts +++ b/server/src/services/recovery/index.ts @@ -42,3 +42,23 @@ export { export type { RunContinuationDecision, } from "./run-liveness-continuations.js"; +export { + DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, + FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + LEGACY_SUCCESSFUL_RUN_HANDOFF_NOTICE_PREFIXES, + SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY, + SUCCESSFUL_RUN_HANDOFF_OPTIONS, + SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY, + SUCCESSFUL_RUN_MISSING_STATE_REASON, + buildFinishSuccessfulRunHandoffIdempotencyKey, + buildSuccessfulRunHandoffExhaustedNotice, + buildSuccessfulRunHandoffInstruction, + buildSuccessfulRunHandoffRequiredNotice, + decideSuccessfulRunHandoff, + findExistingFinishSuccessfulRunHandoffWake, + isSuccessfulRunHandoffRequiredNoticeBody, +} from "./successful-run-handoff.js"; +export type { + SuccessfulRunHandoffNotice, + SuccessfulRunHandoffDecision, +} from "./successful-run-handoff.js"; diff --git a/server/src/services/recovery/model-profile-hint.ts b/server/src/services/recovery/model-profile-hint.ts new file mode 100644 index 00000000000..51e75e44cee --- /dev/null +++ b/server/src/services/recovery/model-profile-hint.ts @@ -0,0 +1,14 @@ +export const RECOVERY_MODEL_PROFILE_KEY = "cheap" as const; + +export function withRecoveryModelProfileHint>( + input: T, +): T & { modelProfile: typeof RECOVERY_MODEL_PROFILE_KEY } { + return { + ...input, + modelProfile: RECOVERY_MODEL_PROFILE_KEY, + }; +} + +export function recoveryAssigneeAdapterOverrides() { + return { modelProfile: RECOVERY_MODEL_PROFILE_KEY }; +} diff --git a/server/src/services/recovery/run-liveness-continuations.ts b/server/src/services/recovery/run-liveness-continuations.ts index b23625c1a90..ecc93e0bc1b 100644 --- a/server/src/services/recovery/run-liveness-continuations.ts +++ b/server/src/services/recovery/run-liveness-continuations.ts @@ -2,6 +2,7 @@ import { and, eq, inArray } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agentWakeupRequests, agents, heartbeatRuns, issues } from "@paperclipai/db"; import type { RunLivenessState } from "@paperclipai/shared"; +import { withRecoveryModelProfileHint } from "./model-profile-hint.js"; import { RECOVERY_REASON_KINDS } from "./origins.js"; export const RUN_LIVENESS_CONTINUATION_REASON = RECOVERY_REASON_KINDS.runLivenessContinuation; @@ -155,7 +156,7 @@ export function decideRunLivenessContinuation(input: { return { kind: "skip", reason: "continuation wake already exists for this source run and attempt" }; } - const payload = { + const payload = withRecoveryModelProfileHint({ issueId: issue.id, sourceRunId: run.id, livenessState, @@ -165,14 +166,14 @@ export function decideRunLivenessContinuation(input: { instruction: nextAction ?? "The previous run ended without concrete progress. Take the first concrete action now or mark the issue blocked with a specific unblock request.", - }; + }); return { kind: "enqueue", nextAttempt, idempotencyKey, payload, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: issue.id, taskId: issue.id, taskKey: issue.id, @@ -183,6 +184,6 @@ export function decideRunLivenessContinuation(input: { livenessContinuationState: livenessState, livenessContinuationReason: livenessReason, livenessContinuationInstruction: payload.instruction, - }, + }), }; } diff --git a/server/src/services/recovery/service.ts b/server/src/services/recovery/service.ts index 23f2922d34e..78d80172400 100644 --- a/server/src/services/recovery/service.ts +++ b/server/src/services/recovery/service.ts @@ -32,6 +32,13 @@ import { instanceSettingsService } from "../instance-settings.js"; import { issueTreeControlService } from "../issue-tree-control.js"; import { issueService } from "../issues.js"; import { getRunLogStore } from "../run-log-store.js"; +import { + DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, + FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + SUCCESSFUL_RUN_MISSING_STATE_REASON, + buildSuccessfulRunHandoffExhaustedNotice, + type SuccessfulRunHandoffNotice, +} from "./successful-run-handoff.js"; import { RECOVERY_ORIGIN_KINDS, buildIssueGraphLivenessLeafKey, @@ -42,6 +49,10 @@ import { classifyIssueGraphLiveness, type IssueLivenessFinding, } from "./issue-graph-liveness.js"; +import { + recoveryAssigneeAdapterOverrides, + withRecoveryModelProfileHint, +} from "./model-profile-hint.js"; import { isAutomaticRecoverySuppressedByPauseHold } from "./pause-hold-guard.js"; const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"] as const; @@ -76,6 +87,16 @@ type LatestIssueRun = Pick< > | null; type SuccessfulLatestIssueRun = NonNullable & { status: "succeeded" }; +type StrandedRecoveryCause = "stranded_assigned_issue" | typeof SUCCESSFUL_RUN_MISSING_STATE_REASON; + +type SuccessfulRunHandoffRecoveryEvidence = { + sourceRunId: string | null; + correctiveRunId: string; + missingDisposition: string; + handoffAttempt: number; + maxHandoffAttempts: number; +}; + type WatchdogDecisionActor = | { type: "board"; userId?: string | null; runId?: string | null } | { type: "agent"; agentId?: string | null; runId?: string | null } @@ -123,6 +144,39 @@ function didAutomaticRecoveryFail( ); } +function successfulRunHandoffRecoveryEvidence(latestRun: LatestIssueRun): SuccessfulRunHandoffRecoveryEvidence | null { + if (!latestRun) return null; + + const context = parseObject(latestRun.contextSnapshot); + const wakeReason = readNonEmptyString(context.wakeReason); + const handoffReason = readNonEmptyString(context.handoffReason); + const isSuccessfulRunHandoff = + wakeReason === FINISH_SUCCESSFUL_RUN_HANDOFF_REASON || + handoffReason === SUCCESSFUL_RUN_MISSING_STATE_REASON || + asBoolean(context.handoffRequired, false) === true; + if (!isSuccessfulRunHandoff) return null; + + const handoffAttempt = asNumber(context.handoffAttempt, 1); + const maxHandoffAttempts = asNumber( + context.maxHandoffAttempts, + DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, + ); + return { + sourceRunId: readNonEmptyString(context.sourceRunId) ?? readNonEmptyString(context.resumeFromRunId), + correctiveRunId: latestRun.id, + missingDisposition: readNonEmptyString(context.missingDisposition) ?? "clear_next_step", + handoffAttempt, + maxHandoffAttempts, + }; +} + +function isExhaustedSuccessfulRunHandoff(latestRun: LatestIssueRun) { + const evidence = successfulRunHandoffRecoveryEvidence(latestRun); + if (!evidence) return null; + if (evidence.handoffAttempt < evidence.maxHandoffAttempts) return { ...evidence, exhausted: false }; + return { ...evidence, exhausted: true }; +} + function issueIdFromRunContext(contextSnapshot: unknown) { const context = parseObject(contextSnapshot); return readNonEmptyString(context.issueId) ?? readNonEmptyString(context.taskId); @@ -145,6 +199,11 @@ function runUiLink(run: { id: string; agentId: string }, prefix: string) { return `[${run.id}](/${prefix}/agents/${run.agentId}/runs/${run.id})`; } +function agentUiLink(agent: { id: string; name: string | null } | null, prefix: string) { + if (!agent) return "unknown"; + return `[${agent.name ?? agent.id}](/${prefix}/agents/${agent.id})`; +} + function formatDuration(ms: number | null) { if (ms === null) return "unknown"; const minutes = Math.floor(ms / 60_000); @@ -391,20 +450,20 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "automation", triggerDetail: "system", reason: input.reason, - payload: { + payload: withRecoveryModelProfileHint({ issueId: input.issueId, ...(input.retryOfRunId ? { retryOfRunId: input.retryOfRunId } : {}), - }, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: input.issueId, taskId: input.issueId, wakeReason: input.reason, retryReason: input.retryReason, source: input.source, ...(input.retryOfRunId ? { retryOfRunId: input.retryOfRunId } : {}), - }, + }), }); if (queued && input.retryOfRunId) { @@ -427,18 +486,18 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "assignment", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: issue.id, mutation: "assigned_todo_liveness_dispatch", - }, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: issue.id, taskId: issue.id, wakeReason: "issue_assigned", source: "issue.assigned_todo_liveness_dispatch", - }, + }), }); } @@ -542,18 +601,18 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "automation", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: candidate.id, mutation: "unassigned_blocker_recovery", - }, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: candidate.id, taskId: candidate.id, wakeReason: "issue_assigned", source: "issue.unassigned_blocker_recovery", - }, + }), }); if (queued) { @@ -995,6 +1054,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) goalId: sourceIssue?.goalId ?? null, billingCode: sourceIssue?.billingCode ?? null, assigneeAgentId: ownerAgentId, + assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides(), originKind: STALE_ACTIVE_RUN_EVALUATION_ORIGIN_KIND, originId: input.run.id, originRunId: input.run.id, @@ -1036,21 +1096,21 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "assignment", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: evaluation.id, staleRunId: input.run.id, sourceIssueId: sourceIssue?.id ?? null, - }, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: evaluation.id, taskId: evaluation.id, wakeReason: "issue_assigned", source: STALE_ACTIVE_RUN_EVALUATION_ORIGIN_KIND, staleRunId: input.run.id, sourceIssueId: sourceIssue?.id ?? null, - }, + }), }); } return { kind: "created" as const, evaluationIssueId: evaluation.id }; @@ -1294,11 +1354,45 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) latestRun: LatestIssueRun; previousStatus: "todo" | "in_progress"; prefix: string; + recoveryCause?: StrandedRecoveryCause; + successfulRunHandoffEvidence?: SuccessfulRunHandoffRecoveryEvidence | null; + sourceAssignee?: Pick | null; }) { const sourceIssue = issueUiLink({ identifier: input.issue.identifier, id: input.issue.id }, input.prefix); const runLink = input.latestRun ? `[\`${input.latestRun.id}\`](/${input.prefix}/agents/${input.latestRun.agentId}/runs/${input.latestRun.id})` : "none"; + if (input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON) { + const sourceRunId = input.successfulRunHandoffEvidence?.sourceRunId; + const sourceRunLink = sourceRunId && input.latestRun + ? `[\`${sourceRunId}\`](/${input.prefix}/agents/${input.latestRun.agentId}/runs/${sourceRunId})` + : "unknown"; + const missingDisposition = input.successfulRunHandoffEvidence?.missingDisposition ?? "clear_next_step"; + return [ + "Paperclip exhausted the bounded corrective handoff for a successful run that still has no valid issue disposition.", + "", + "This is not a runtime/adapter crash report. The source run succeeded; the remaining problem is the missing `done`, `in_review`, `blocked`, delegated follow-up, or explicit continuation path.", + "", + "## Safe Evidence", + "", + `- Source issue: ${sourceIssue}`, + `- Source run: ${sourceRunLink}`, + `- Corrective handoff run: ${runLink}`, + `- Source assignee: ${agentUiLink(input.sourceAssignee ?? null, input.prefix)}`, + `- Latest issue status: \`${input.issue.status}\``, + `- Latest handoff run status: \`${input.latestRun?.status ?? "unknown"}\``, + `- Normalized cause: \`${SUCCESSFUL_RUN_MISSING_STATE_REASON}\``, + `- Missing disposition: \`${missingDisposition}\``, + "- Suggested manager action: choose and record a valid issue disposition without copying transcript content.", + "", + "## Required Action", + "", + "- Inspect the source issue and run metadata, not raw transcript excerpts.", + "- Choose a valid issue disposition: `done`/`cancelled`, `in_review` with an owner, `blocked` with first-class blockers, delegated follow-up work, or an explicit continuation path.", + "- When the source issue has a clear owner and disposition, mark this recovery issue done.", + ].join("\n"); + } + const retryReason = readNonEmptyString(parseObject(input.latestRun?.contextSnapshot)?.retryReason) ?? "unknown"; const failureSummary = summarizeRunFailureForIssueComment(input.latestRun); @@ -1331,6 +1425,8 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) issue: typeof issues.$inferSelect; latestRun: LatestIssueRun; previousStatus: "todo" | "in_progress"; + recoveryCause?: StrandedRecoveryCause; + successfulRunHandoffEvidence?: SuccessfulRunHandoffRecoveryEvidence | null; }) { if (isStrandedIssueRecoveryIssue(input.issue)) return null; @@ -1341,15 +1437,22 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) if (!ownerAgentId) return null; const prefix = await getCompanyIssuePrefix(input.issue.companyId); + const sourceAssignee = input.issue.assigneeAgentId ? await getAgent(input.issue.assigneeAgentId) : null; + const recoveryCause = input.recoveryCause ?? "stranded_assigned_issue"; let recovery: Awaited>; try { recovery = await issuesSvc.create(input.issue.companyId, { - title: `Recover stalled issue ${input.issue.identifier ?? input.issue.title}`, + title: recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON + ? `Recover missing next step ${input.issue.identifier ?? input.issue.title}` + : `Recover stalled issue ${input.issue.identifier ?? input.issue.title}`, description: buildStrandedIssueRecoveryDescription({ issue: input.issue, latestRun: input.latestRun, previousStatus: input.previousStatus, prefix, + recoveryCause, + successfulRunHandoffEvidence: input.successfulRunHandoffEvidence, + sourceAssignee, }), status: "todo", priority: input.issue.priority, @@ -1357,6 +1460,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) projectId: input.issue.projectId, goalId: input.issue.goalId, assigneeAgentId: ownerAgentId, + assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides(), originKind: STRANDED_ISSUE_RECOVERY_ORIGIN_KIND, originId: input.issue.id, originRunId: input.latestRun?.id ?? null, @@ -1364,6 +1468,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) STRANDED_ISSUE_RECOVERY_ORIGIN_KIND, input.issue.companyId, input.issue.id, + recoveryCause, input.latestRun?.id ?? "no-run", ].join(":"), billingCode: input.issue.billingCode, @@ -1380,21 +1485,23 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "assignment", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: recovery.id, sourceIssueId: input.issue.id, strandedRunId: input.latestRun?.id ?? null, - }, + recoveryCause, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: recovery.id, taskId: recovery.id, wakeReason: "issue_assigned", source: STRANDED_ISSUE_RECOVERY_ORIGIN_KIND, sourceIssueId: input.issue.id, strandedRunId: input.latestRun?.id ?? null, - }, + recoveryCause, + }), }); return recovery; @@ -1512,7 +1619,9 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) issue: typeof issues.$inferSelect; previousStatus: "todo" | "in_progress"; latestRun: LatestIssueRun; - comment: string; + comment?: string; + recoveryCause?: StrandedRecoveryCause; + successfulRunHandoffEvidence?: SuccessfulRunHandoffRecoveryEvidence | null; }) { if (isStrandedIssueRecoveryIssue(input.issue)) { return escalateStrandedRecoveryIssueInPlace({ @@ -1526,6 +1635,8 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) issue: input.issue, previousStatus: input.previousStatus, latestRun: input.latestRun, + recoveryCause: input.recoveryCause, + successfulRunHandoffEvidence: input.successfulRunHandoffEvidence, }); const blockerIds = await existingUnresolvedBlockerIssueIds(input.issue.companyId, input.issue.id); const nextBlockerIds = recoveryIssue @@ -1538,10 +1649,29 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) if (!updated) return null; const prefix = await getCompanyIssuePrefix(input.issue.companyId); + const recoveryOwner = recoveryIssue?.assigneeAgentId ? await getAgent(recoveryIssue.assigneeAgentId) : null; + const sourceAssignee = input.issue.assigneeAgentId ? await getAgent(input.issue.assigneeAgentId) : null; + let notice: SuccessfulRunHandoffNotice | null = null; + if (input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON && input.successfulRunHandoffEvidence) { + notice = buildSuccessfulRunHandoffExhaustedNotice({ + issue: input.issue, + sourceRun: input.successfulRunHandoffEvidence.sourceRunId + ? { id: input.successfulRunHandoffEvidence.sourceRunId, status: "succeeded" } + : null, + correctiveRun: input.latestRun ? { id: input.latestRun.id, status: input.latestRun.status } : null, + sourceAssignee, + recoveryIssue, + recoveryOwner, + latestIssueStatus: input.issue.status, + latestHandoffRunStatus: input.latestRun?.status ?? "unknown", + missingDisposition: input.successfulRunHandoffEvidence.missingDisposition, + }); + } const recoveryLine = recoveryIssue ? [ "", `- Recovery issue: ${issueUiLink({ identifier: recoveryIssue.identifier, id: recoveryIssue.id }, prefix)}`, + `- Recovery owner: ${agentUiLink(recoveryOwner, prefix)}`, "- Next action: the recovery owner should either restore a live execution path or record the manual resolution, then mark the recovery issue done.", ].join("\n") : [ @@ -1550,7 +1680,15 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) "- Next action: a board operator should assign an invokable recovery owner, fix the agent/runtime state, or record an intentional manual resolution.", ].join("\n"); - await issuesSvc.addComment(input.issue.id, `${input.comment}${recoveryLine}`, {}); + if (notice) { + await issuesSvc.addComment(input.issue.id, notice.body, {}, { + authorType: "system", + presentation: notice.presentation, + metadata: notice.metadata, + }); + } else { + await issuesSvc.addComment(input.issue.id, `${input.comment ?? ""}${recoveryLine}`, {}); + } await logActivity(db, { companyId: input.issue.companyId, @@ -1558,14 +1696,19 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) actorId: "system", agentId: null, runId: null, - action: "issue.updated", + action: input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON + ? "issue.successful_run_handoff_escalated" + : "issue.updated", entityType: "issue", entityId: input.issue.id, details: { identifier: input.issue.identifier, status: "blocked", previousStatus: input.previousStatus, - source: "recovery.reconcile_stranded_assigned_issue", + source: input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON + ? "recovery.reconcile_successful_run_handoff_missing_state" + : "recovery.reconcile_stranded_assigned_issue", + recoveryCause: input.recoveryCause ?? "stranded_assigned_issue", latestRunId: input.latestRun?.id ?? null, latestRunStatus: input.latestRun?.status ?? null, latestRunErrorCode: input.latestRun?.errorCode ?? null, @@ -1596,6 +1739,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) productiveContinuationObserved: 0, successfulContinuationObserved: 0, orphanBlockersAssigned: 0, + successfulRunHandoffEscalated: 0, escalated: 0, skipped: 0, issueIds: [] as string[], @@ -1713,6 +1857,28 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) result.skipped += 1; continue; } + const handoffEvidence = isExhaustedSuccessfulRunHandoff(latestRun); + if (handoffEvidence) { + if (!handoffEvidence.exhausted) { + result.skipped += 1; + continue; + } + + const updated = await escalateStrandedAssignedIssue({ + issue, + previousStatus: "in_progress", + latestRun, + recoveryCause: SUCCESSFUL_RUN_MISSING_STATE_REASON, + successfulRunHandoffEvidence: handoffEvidence, + }); + if (updated) { + result.successfulRunHandoffEscalated += 1; + result.issueIds.push(issue.id); + } else { + result.skipped += 1; + } + continue; + } if (isSuccessfulInProgressContinuationRun(latestRun)) { const successfulRun = latestRun; @@ -2393,6 +2559,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) projectId: recoveryIssue.projectId, goalId: recoveryIssue.goalId, assigneeAgentId: ownerSelection.agentId, + assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides(), originKind: RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation, originId: input.finding.incidentKey, originFingerprint: livenessRecoveryLeafFingerprint(input.finding), @@ -2473,15 +2640,15 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) source: "assignment", triggerDetail: "system", reason: "issue_assigned", - payload: { + payload: withRecoveryModelProfileHint({ issueId: escalation.id, sourceIssueId: issue.id, recoveryIssueId: recoveryIssue.id, incidentKey: input.finding.incidentKey, - }, + }), requestedByActorType: "system", requestedByActorId: null, - contextSnapshot: { + contextSnapshot: withRecoveryModelProfileHint({ issueId: escalation.id, taskId: escalation.id, wakeReason: "issue_assigned", @@ -2489,7 +2656,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) sourceIssueId: issue.id, recoveryIssueId: recoveryIssue.id, incidentKey: input.finding.incidentKey, - }, + }), }); logger.warn({ diff --git a/server/src/services/recovery/successful-run-handoff.test.ts b/server/src/services/recovery/successful-run-handoff.test.ts new file mode 100644 index 00000000000..b5e25b74516 --- /dev/null +++ b/server/src/services/recovery/successful-run-handoff.test.ts @@ -0,0 +1,295 @@ +import { describe, expect, it } from "vitest"; +import { + FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY, + SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY, + SUCCESSFUL_RUN_MISSING_STATE_REASON, + buildFinishSuccessfulRunHandoffIdempotencyKey, + buildSuccessfulRunHandoffExhaustedNotice, + buildSuccessfulRunHandoffRequiredNotice, + decideSuccessfulRunHandoff, + isIdempotentFinishSuccessfulRunHandoffWakeStatus, + isSuccessfulRunHandoffRequiredNoticeBody, +} from "./successful-run-handoff.js"; + +const run = { + id: "run-1", + companyId: "company-1", + agentId: "agent-1", + status: "succeeded", + contextSnapshot: { issueId: "issue-1" }, +} as any; + +const issue = { + id: "issue-1", + companyId: "company-1", + identifier: "PAP-1", + title: "Finish backend handoff", + status: "in_progress", + assigneeAgentId: "agent-1", + assigneeUserId: null, + executionState: null, +} as any; + +const agent = { + id: "agent-1", + companyId: "company-1", + status: "idle", +} as any; + +function decide(overrides: Partial[0]> = {}) { + return decideSuccessfulRunHandoff({ + run, + issue, + agent, + livenessState: "advanced", + detectedProgressSummary: "Run produced concrete action evidence: 1 issue comment(s)", + taskKey: "issue-1", + hasActiveExecutionPath: false, + hasQueuedWake: false, + hasPendingInteractionOrApproval: false, + hasExplicitBlockerPath: false, + hasOpenRecoveryIssue: false, + hasPauseHold: false, + budgetBlocked: false, + idempotentWakeExists: false, + ...overrides, + }); +} + +describe("successful run handoff decision", () => { + it("queues one corrective handoff wake for a successful progress run without a visible next action", () => { + const decision = decide(); + + expect(decision.kind).toBe("enqueue"); + if (decision.kind !== "enqueue") return; + expect(decision.idempotencyKey).toBe("finish_successful_run_handoff:issue-1:run-1:1"); + expect(decision.payload).toMatchObject({ + issueId: "issue-1", + sourceRunId: "run-1", + handoffRequired: true, + handoffReason: SUCCESSFUL_RUN_MISSING_STATE_REASON, + missingDisposition: "clear_next_step", + handoffAttempt: 1, + maxHandoffAttempts: 1, + resumeIntent: true, + resumeFromRunId: "run-1", + modelProfile: "cheap", + }); + expect(decision.contextSnapshot).toMatchObject({ + wakeReason: FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + handoffRequired: true, + modelProfile: "cheap", + }); + expect(decision.instruction).toContain("Resolve the missing disposition before creating or revising any new artifacts"); + expect(decision.instruction).toContain("Choose **exactly one** outcome"); + expect(decision.instruction).toContain("record an explicit continuation path"); + }); + + it("does not queue when the issue already has a valid disposition", () => { + expect(decide({ issue: { ...issue, status: "done" } as any })).toEqual({ + kind: "skip", + reason: "issue status done is a valid disposition", + }); + }); + + it("does not queue when a successful run records an accepted next-action path", () => { + expect(decide({ issue: { ...issue, status: "in_review" } as any })).toEqual({ + kind: "skip", + reason: "issue status in_review is a valid disposition", + }); + expect(decide({ issue: { ...issue, status: "blocked" } as any })).toEqual({ + kind: "skip", + reason: "issue status blocked is a valid disposition", + }); + expect(decide({ hasPendingInteractionOrApproval: true })).toEqual({ + kind: "skip", + reason: "pending interaction or approval owns the next action", + }); + expect(decide({ hasActiveExecutionPath: true })).toEqual({ + kind: "skip", + reason: "issue already has an active execution path", + }); + }); + + it("does not queue when another wake or dependency path already owns the next action", () => { + expect(decide({ hasQueuedWake: true })).toEqual({ + kind: "skip", + reason: "issue already has a queued or deferred wake", + }); + expect(decide({ hasExplicitBlockerPath: true })).toEqual({ + kind: "skip", + reason: "explicit blocker path owns the next action", + }); + }); + + it("does not queue when a successful run has no progress signal", () => { + expect(decide({ livenessState: null, detectedProgressSummary: null })).toEqual({ + kind: "skip", + reason: "successful run did not produce handoff-relevant progress", + }); + }); + + it("does not treat adapter or runtime failures as missing-disposition handoffs", () => { + expect(decide({ run: { ...run, status: "failed", errorCode: "adapter_failed" } as any })).toEqual({ + kind: "skip", + reason: "source run did not succeed", + }); + }); + + it("does not queue on missing-comment retry bookkeeping runs", () => { + expect(decide({ run: { ...run, issueCommentStatus: "retry_exhausted" } as any })).toEqual({ + kind: "skip", + reason: "missing issue comment retry owns the next action", + }); + }); + + it("does not loop from a corrective handoff run", () => { + expect(decide({ + run: { + ...run, + id: "run-2", + contextSnapshot: { + issueId: "issue-1", + wakeReason: FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + handoffRequired: true, + }, + } as any, + })).toEqual({ + kind: "skip", + reason: "source run is already a corrective handoff run", + }); + }); + + it("does not queue for issue monitor maintenance runs", () => { + expect(decide({ + run: { + ...run, + contextSnapshot: { + issueId: "issue-1", + source: "issue.monitor", + wakeReason: "issue_monitor_due", + }, + } as any, + })).toEqual({ + kind: "skip", + reason: "issue monitor run owns its own recovery path", + }); + }); + + it("uses a stable one-attempt idempotency key", () => { + expect(buildFinishSuccessfulRunHandoffIdempotencyKey({ + issueId: "issue-1", + sourceRunId: "run-1", + })).toBe("finish_successful_run_handoff:issue-1:run-1:1"); + }); + + it("allows failed or cancelled corrective wakes to be retried", () => { + expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("queued")).toBe(true); + expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("claimed")).toBe(true); + expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("completed")).toBe(true); + expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("failed")).toBe(false); + expect(isIdempotentFinishSuccessfulRunHandoffWakeStatus("cancelled")).toBe(false); + }); + + it("builds the required system notice with hidden structured metadata", () => { + const notice = buildSuccessfulRunHandoffRequiredNotice({ + issue: { + id: "11111111-1111-4111-8111-111111111111", + identifier: "PAP-1", + title: "Finish backend handoff", + status: "in_progress", + } as any, + run: { + id: "22222222-2222-4222-8222-222222222222", + status: "succeeded", + } as any, + agent: { + id: "33333333-3333-4333-8333-333333333333", + name: "CodexCoder", + } as any, + detectedProgressSummary: "Run produced concrete action evidence: 1 issue comment(s)", + }); + + expect(notice.body).toBe(SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY); + expect(notice.presentation).toEqual({ + kind: "system_notice", + tone: "warning", + title: "Missing issue disposition", + detailsDefaultOpen: false, + }); + expect(notice.metadata.sections).toEqual(expect.arrayContaining([ + expect.objectContaining({ + title: "Required action", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "issue_link", identifier: "PAP-1" }), + expect.objectContaining({ type: "agent_link", name: "CodexCoder" }), + expect.objectContaining({ type: "key_value", label: "Missing disposition", value: "clear_next_step" }), + ]), + }), + expect.objectContaining({ + title: "Run evidence", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "run_link", runId: "22222222-2222-4222-8222-222222222222" }), + expect.objectContaining({ type: "key_value", label: "Normalized cause", value: SUCCESSFUL_RUN_MISSING_STATE_REASON }), + expect.objectContaining({ type: "key_value", label: "Detected progress" }), + ]), + }), + ])); + }); + + it("builds the exhausted system notice with recovery metadata", () => { + const notice = buildSuccessfulRunHandoffExhaustedNotice({ + issue: { + id: "11111111-1111-4111-8111-111111111111", + identifier: "PAP-1", + title: "Finish backend handoff", + status: "in_progress", + } as any, + sourceRun: { id: "22222222-2222-4222-8222-222222222222", status: "succeeded" } as any, + correctiveRun: { id: "44444444-4444-4444-8444-444444444444", status: "failed" } as any, + sourceAssignee: { id: "33333333-3333-4333-8333-333333333333", name: "CodexCoder" } as any, + recoveryIssue: { + id: "55555555-5555-4555-8555-555555555555", + identifier: "PAP-2", + title: "Recover missing next step PAP-1", + status: "todo", + } as any, + recoveryOwner: { id: "66666666-6666-4666-8666-666666666666", name: "CTO" } as any, + latestIssueStatus: "in_progress", + latestHandoffRunStatus: "failed", + missingDisposition: "clear_next_step", + }); + + expect(notice.body).toBe(SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY); + expect(notice.presentation).toMatchObject({ + kind: "system_notice", + tone: "danger", + detailsDefaultOpen: false, + }); + expect(notice.metadata.sections).toEqual(expect.arrayContaining([ + expect.objectContaining({ + title: "Recovery owner", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "issue_link", identifier: "PAP-2" }), + expect.objectContaining({ type: "agent_link", label: "Recovery owner", name: "CTO" }), + ]), + }), + expect.objectContaining({ + title: "Run evidence", + rows: expect.arrayContaining([ + expect.objectContaining({ type: "run_link", label: "Source run" }), + expect.objectContaining({ type: "run_link", label: "Corrective handoff run" }), + expect.objectContaining({ type: "key_value", label: "Missing disposition", value: "clear_next_step" }), + ]), + }), + ])); + }); + + it("recognizes new notices and legacy markdown headings for fallback deduplication", () => { + expect(isSuccessfulRunHandoffRequiredNoticeBody(SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY)).toBe(true); + expect(isSuccessfulRunHandoffRequiredNoticeBody("## Successful run missing issue disposition\n\nold body")).toBe(true); + expect(isSuccessfulRunHandoffRequiredNoticeBody("## This issue still needs a next step\n\nold body")).toBe(true); + expect(isSuccessfulRunHandoffRequiredNoticeBody("Unrelated comment")).toBe(false); + }); +}); diff --git a/server/src/services/recovery/successful-run-handoff.ts b/server/src/services/recovery/successful-run-handoff.ts new file mode 100644 index 00000000000..2d5b79ff0e3 --- /dev/null +++ b/server/src/services/recovery/successful-run-handoff.ts @@ -0,0 +1,405 @@ +import { and, eq, inArray } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { agentWakeupRequests, agents, heartbeatRuns, issues } from "@paperclipai/db"; +import type { IssueCommentMetadata, IssueCommentPresentation, RunLivenessState } from "@paperclipai/shared"; +import { withRecoveryModelProfileHint } from "./model-profile-hint.js"; + +export const FINISH_SUCCESSFUL_RUN_HANDOFF_REASON = "finish_successful_run_handoff"; +export const SUCCESSFUL_RUN_MISSING_STATE_REASON = "successful_run_missing_state"; +export const DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS = 1; +export const SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY = + "Paperclip needs a disposition before this issue can continue."; +export const SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY = + "Paperclip could not resolve this issue's missing disposition automatically. The issue is blocked on a recovery owner."; +export const LEGACY_SUCCESSFUL_RUN_HANDOFF_NOTICE_PREFIXES = [ + "## This issue still needs a next step", + "## Successful run missing issue disposition", +] as const; + +export const SUCCESSFUL_RUN_HANDOFF_OPTIONS = [ + "mark_done_or_cancelled", + "send_for_review_or_ask_for_input", + "mark_blocked", + "delegate_or_continue_from_checkpoint", +] as const; + +const PRODUCTIVE_SUCCESS_LIVENESS_STATES = new Set([ + "advanced", + "completed", + "blocked", + "needs_followup", +]); + +const IDEMPOTENT_HANDOFF_WAKE_STATUSES = [ + "queued", + "deferred_issue_execution", + "claimed", + "completed", +]; +const IDEMPOTENT_HANDOFF_WAKE_STATUS_SET = new Set(IDEMPOTENT_HANDOFF_WAKE_STATUSES); + +export function isIdempotentFinishSuccessfulRunHandoffWakeStatus(status: string) { + return IDEMPOTENT_HANDOFF_WAKE_STATUS_SET.has(status); +} + +type HeartbeatRunRow = typeof heartbeatRuns.$inferSelect; +type IssueRow = Pick< + typeof issues.$inferSelect, + "id" | "companyId" | "identifier" | "title" | "status" | "assigneeAgentId" | "assigneeUserId" | "executionState" +>; +type AgentRow = Pick; +type NoticeIssue = Pick; +type NoticeRun = Pick; +type NoticeAgent = Pick; +type NullableNoticeAgent = NoticeAgent | null | undefined; +type NullableNoticeIssue = NoticeIssue | null | undefined; +type NullableNoticeRun = NoticeRun | null | undefined; + +export type SuccessfulRunHandoffNotice = { + body: string; + presentation: IssueCommentPresentation; + metadata: IssueCommentMetadata; +}; + +export type SuccessfulRunHandoffDecision = + | { + kind: "enqueue"; + idempotencyKey: string; + payload: Record; + contextSnapshot: Record; + instruction: string; + } + | { + kind: "skip"; + reason: string; + }; + +function metadataText(value: unknown, fallback = "unknown") { + const text = typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim(); + const resolved = text.length > 0 ? text : fallback; + return resolved.length > 2000 ? `${resolved.slice(0, 1997)}...` : resolved; +} + +function keyValueRow(label: string, value: unknown): IssueCommentMetadata["sections"][number]["rows"][number] { + return { type: "key_value", label, value: metadataText(value) }; +} + +function issueLinkRow( + label: string, + issue: NullableNoticeIssue, +): IssueCommentMetadata["sections"][number]["rows"][number] { + if (!issue) return keyValueRow(label, "unknown"); + return { + type: "issue_link", + label, + issueId: issue.id, + identifier: issue.identifier, + title: issue.title, + }; +} + +function runLinkRow( + label: string, + run: NullableNoticeRun, +): IssueCommentMetadata["sections"][number]["rows"][number] { + if (!run) return keyValueRow(label, "unknown"); + return { type: "run_link", label, runId: run.id, title: run.status }; +} + +function agentLinkRow( + label: string, + agent: NullableNoticeAgent, +): IssueCommentMetadata["sections"][number]["rows"][number] { + if (!agent) return keyValueRow(label, "unknown"); + return { type: "agent_link", label, agentId: agent.id, name: agent.name }; +} + +function systemNoticePresentation(input: { + tone: IssueCommentPresentation["tone"]; + title: string; +}): IssueCommentPresentation { + return { + kind: "system_notice", + tone: input.tone, + title: input.title, + detailsDefaultOpen: false, + }; +} + +export function isSuccessfulRunHandoffRequiredNoticeBody(body: string) { + const trimmed = body.trim(); + return trimmed === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY || + LEGACY_SUCCESSFUL_RUN_HANDOFF_NOTICE_PREFIXES.some((prefix) => trimmed.startsWith(prefix)); +} + +export function buildSuccessfulRunHandoffRequiredNotice(input: { + issue: NoticeIssue; + run: NoticeRun; + agent: NoticeAgent; + detectedProgressSummary: string; +}): SuccessfulRunHandoffNotice { + return { + body: SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY, + presentation: systemNoticePresentation({ + tone: "warning", + title: "Missing issue disposition", + }), + metadata: { + version: 1, + sections: [ + { + title: "Required action", + rows: [ + issueLinkRow("Source issue", input.issue), + agentLinkRow("Assignee", input.agent), + keyValueRow("Missing disposition", "clear_next_step"), + keyValueRow( + "Valid dispositions", + "done, cancelled, in_review with an owner, blocked with blockers, delegated follow-up, or explicit continuation", + ), + ], + }, + { + title: "Run evidence", + rows: [ + runLinkRow("Successful run", input.run), + keyValueRow("Run status", input.run.status), + keyValueRow("Normalized cause", SUCCESSFUL_RUN_MISSING_STATE_REASON), + keyValueRow("Detected progress", input.detectedProgressSummary), + keyValueRow("Automatic retry", "one corrective handoff wake queued"), + ], + }, + ], + }, + }; +} + +export function buildSuccessfulRunHandoffExhaustedNotice(input: { + issue: NoticeIssue; + sourceRun: NullableNoticeRun; + correctiveRun: NullableNoticeRun; + sourceAssignee: NullableNoticeAgent; + recoveryIssue: NullableNoticeIssue; + recoveryOwner: NullableNoticeAgent; + latestIssueStatus: string; + latestHandoffRunStatus: string; + missingDisposition: string; +}): SuccessfulRunHandoffNotice { + return { + body: SUCCESSFUL_RUN_HANDOFF_EXHAUSTED_NOTICE_BODY, + presentation: systemNoticePresentation({ + tone: "danger", + title: "Missing disposition recovery blocked", + }), + metadata: { + version: 1, + sections: [ + { + title: "Recovery owner", + rows: [ + issueLinkRow("Source issue", input.issue), + issueLinkRow("Recovery issue", input.recoveryIssue), + agentLinkRow("Recovery owner", input.recoveryOwner), + agentLinkRow("Source assignee", input.sourceAssignee), + keyValueRow("Suggested action", "choose and record a valid issue disposition without copying transcript content"), + ], + }, + { + title: "Run evidence", + rows: [ + runLinkRow("Source run", input.sourceRun), + runLinkRow("Corrective handoff run", input.correctiveRun), + keyValueRow("Latest issue status", input.latestIssueStatus), + keyValueRow("Latest handoff run status", input.latestHandoffRunStatus), + keyValueRow("Normalized cause", SUCCESSFUL_RUN_MISSING_STATE_REASON), + keyValueRow("Missing disposition", input.missingDisposition), + ], + }, + ], + }, + }; +} + +export function buildFinishSuccessfulRunHandoffIdempotencyKey(input: { + issueId: string; + sourceRunId: string; + attempt?: number; +}) { + return [ + FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + input.issueId, + input.sourceRunId, + String(input.attempt ?? 1), + ].join(":"); +} + +export async function findExistingFinishSuccessfulRunHandoffWake( + db: Db, + input: { + companyId: string; + idempotencyKey: string; + }, +) { + return db + .select({ id: agentWakeupRequests.id, status: agentWakeupRequests.status }) + .from(agentWakeupRequests) + .where( + and( + eq(agentWakeupRequests.companyId, input.companyId), + eq(agentWakeupRequests.idempotencyKey, input.idempotencyKey), + inArray(agentWakeupRequests.status, IDEMPOTENT_HANDOFF_WAKE_STATUSES), + ), + ) + .limit(1) + .then((rows) => rows[0] ?? null); +} + +function readRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? value as Record + : {}; +} + +function readString(value: unknown) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function isCorrectiveHandoffRun(run: HeartbeatRunRow) { + const context = readRecord(run.contextSnapshot); + return context.handoffRequired === true || + readString(context.wakeReason) === FINISH_SUCCESSFUL_RUN_HANDOFF_REASON; +} + +function isIssueMonitorMaintenanceRun(run: HeartbeatRunRow) { + const context = readRecord(run.contextSnapshot); + const wakeReason = readString(context.wakeReason); + const source = readString(context.source); + return Boolean(wakeReason?.startsWith("issue_monitor") || source?.startsWith("issue.monitor")); +} + +function isProductiveSuccessfulRun(input: { + livenessState: RunLivenessState | null; + detectedProgressSummary: string | null; +}) { + if (input.livenessState && PRODUCTIVE_SUCCESS_LIVENESS_STATES.has(input.livenessState)) return true; + return Boolean(input.detectedProgressSummary); +} + +export function buildSuccessfulRunHandoffInstruction(input: { + issueIdentifier: string | null; + sourceRunId: string; +}) { + const issueLabel = input.issueIdentifier ?? "this issue"; + return [ + `Your previous run on ${issueLabel} succeeded, but the issue is still in \`in_progress\` and Paperclip cannot identify a valid issue disposition.`, + "", + "Resolve the missing disposition before creating or revising any new artifacts. Choose **exactly one** outcome and perform the matching Paperclip action:", + "", + "**Is the issue finished?**", + "1. Mark it `done` (scope complete) or `cancelled` (intentionally stopped).", + "", + "**Does someone else need to look at it?**", + "2. Move it to `in_review` with a real reviewer path — `executionState.currentParticipant`, a human owner via `assigneeUserId`, a pending issue-thread interaction, or a linked pending approval.", + "", + "**Can it not continue right now?**", + "3. Mark it `blocked` with first-class blockers (`blockedByIssueIds`) or a clearly named unblock owner/action.", + "", + "**Is there more work to do?**", + `4. Either delegate follow-up work (create/link a follow-up issue and block this one on it, or close this issue if its scope is independently complete) or record an explicit continuation path with \`resumeIntent: true\`, \`resumeFromRunId: ${input.sourceRunId}\`, and a concrete next action.`, + "", + "Comments, document revisions, work-product writes, and continuation summaries are supporting evidence only — they do not satisfy this handoff unless the issue state/path also records one valid disposition.", + ].join("\n"); +} + +export function decideSuccessfulRunHandoff(input: { + run: HeartbeatRunRow; + issue: IssueRow | null; + agent: AgentRow | null; + livenessState: RunLivenessState | null; + detectedProgressSummary: string | null; + taskKey: string | null; + hasActiveExecutionPath: boolean; + hasQueuedWake: boolean; + hasPendingInteractionOrApproval: boolean; + hasExplicitBlockerPath: boolean; + hasOpenRecoveryIssue: boolean; + hasPauseHold: boolean; + budgetBlocked: boolean; + idempotentWakeExists: boolean; +}): SuccessfulRunHandoffDecision { + const { run, issue, agent } = input; + + if (run.status !== "succeeded") return { kind: "skip", reason: "source run did not succeed" }; + if (isCorrectiveHandoffRun(run)) return { kind: "skip", reason: "source run is already a corrective handoff run" }; + if (isIssueMonitorMaintenanceRun(run)) return { kind: "skip", reason: "issue monitor run owns its own recovery path" }; + if (run.issueCommentStatus === "retry_queued" || run.issueCommentStatus === "retry_exhausted") { + return { kind: "skip", reason: "missing issue comment retry owns the next action" }; + } + if (!issue) return { kind: "skip", reason: "issue not found" }; + if (!agent) return { kind: "skip", reason: "agent not found" }; + if (issue.companyId !== run.companyId || agent.companyId !== run.companyId) { + return { kind: "skip", reason: "company scope mismatch" }; + } + if (issue.assigneeAgentId !== run.agentId) { + return { kind: "skip", reason: "issue is no longer assigned to the source run agent" }; + } + if (issue.assigneeUserId) return { kind: "skip", reason: "issue is human-owned" }; + if (issue.status !== "in_progress") return { kind: "skip", reason: `issue status ${issue.status} is a valid disposition` }; + if (issue.executionState) return { kind: "skip", reason: "issue has execution policy state" }; + if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") { + return { kind: "skip", reason: `agent status ${agent.status} is not invokable` }; + } + if (!isProductiveSuccessfulRun(input)) { + return { kind: "skip", reason: "successful run did not produce handoff-relevant progress" }; + } + if (input.hasActiveExecutionPath) return { kind: "skip", reason: "issue already has an active execution path" }; + if (input.hasQueuedWake) return { kind: "skip", reason: "issue already has a queued or deferred wake" }; + if (input.hasPendingInteractionOrApproval) { + return { kind: "skip", reason: "pending interaction or approval owns the next action" }; + } + if (input.hasExplicitBlockerPath) return { kind: "skip", reason: "explicit blocker path owns the next action" }; + if (input.hasOpenRecoveryIssue) return { kind: "skip", reason: "open recovery issue owns the ambiguity" }; + if (input.hasPauseHold) return { kind: "skip", reason: "issue is under an active pause hold" }; + if (input.budgetBlocked) return { kind: "skip", reason: "budget hard stop blocks corrective wake" }; + if (input.idempotentWakeExists) { + return { kind: "skip", reason: "corrective handoff wake already exists for this source run" }; + } + + const instruction = buildSuccessfulRunHandoffInstruction({ + issueIdentifier: issue.identifier, + sourceRunId: run.id, + }); + const payload = withRecoveryModelProfileHint({ + issueId: issue.id, + taskId: issue.id, + sourceIssueId: issue.id, + sourceRunId: run.id, + handoffRequired: true, + handoffReason: SUCCESSFUL_RUN_MISSING_STATE_REASON, + missingDisposition: "clear_next_step", + validDispositionOptions: [...SUCCESSFUL_RUN_HANDOFF_OPTIONS], + detectedProgressSummary: input.detectedProgressSummary, + handoffAttempt: 1, + maxHandoffAttempts: DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, + resumeIntent: true, + followUpRequested: true, + resumeFromRunId: run.id, + ...(input.taskKey ? { taskKey: input.taskKey } : {}), + instruction, + }); + + return { + kind: "enqueue", + idempotencyKey: buildFinishSuccessfulRunHandoffIdempotencyKey({ + issueId: issue.id, + sourceRunId: run.id, + }), + payload, + instruction, + contextSnapshot: withRecoveryModelProfileHint({ + ...payload, + wakeReason: FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, + livenessState: input.livenessState, + }), + }; +} diff --git a/ui/src/components/CommentThread.test.tsx b/ui/src/components/CommentThread.test.tsx index 18e77a4328a..b1f3d741a95 100644 --- a/ui/src/components/CommentThread.test.tsx +++ b/ui/src/components/CommentThread.test.tsx @@ -192,6 +192,9 @@ describe("CommentThread", () => { authorAgentId: null, authorUserId: "local-board", body: "Please continue validation.", + authorType: "user", + presentation: null, + metadata: null, followUpRequested: true, createdAt: new Date("2026-03-11T10:00:00.000Z"), updatedAt: new Date("2026-03-11T10:00:00.000Z"), @@ -349,6 +352,9 @@ describe("CommentThread", () => { authorAgentId: null, authorUserId: "user-1", body: "Hello from the comment body", + authorType: "user", + presentation: null, + metadata: null, createdAt: new Date("2026-03-11T11:00:00.000Z"), updatedAt: new Date("2026-03-11T11:00:00.000Z"), }]} diff --git a/ui/src/components/IssueBlockedNotice.test.tsx b/ui/src/components/IssueBlockedNotice.test.tsx new file mode 100644 index 00000000000..c09e9f64bfc --- /dev/null +++ b/ui/src/components/IssueBlockedNotice.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import type { AnchorHTMLAttributes, ReactElement } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { IssueBlockedNotice } from "./IssueBlockedNotice"; + +vi.mock("@/lib/router", () => ({ + Link: ({ children, to, ...props }: AnchorHTMLAttributes & { to: string }) => ( + {children} + ), +})); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +let root: ReturnType | null = null; +let container: HTMLDivElement | null = null; + +afterEach(() => { + if (root) { + act(() => root?.unmount()); + } + root = null; + container?.remove(); + container = null; +}); + +function render(element: ReactElement) { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + act(() => root?.render(element)); + return container; +} + +describe("IssueBlockedNotice", () => { + it("renders a successful-run next-step notice without requiring blockers", () => { + const node = render( + , + ); + + expect(node.textContent).toContain("This issue still needs a next step."); + expect(node.textContent).toContain("Corrective wake queued for CodexCoder"); + expect(node.textContent).toContain("Detected progress: Updated the plan"); + expect(node.textContent).not.toContain("Work on this issue is blocked until"); + expect(node.querySelector('[data-successful-run-handoff="required"]')).not.toBeNull(); + }); +}); diff --git a/ui/src/components/IssueBlockedNotice.tsx b/ui/src/components/IssueBlockedNotice.tsx index e57c7c3bd6a..e1283fd31ab 100644 --- a/ui/src/components/IssueBlockedNotice.tsx +++ b/ui/src/components/IssueBlockedNotice.tsx @@ -1,5 +1,6 @@ -import type { IssueBlockerAttention, IssueRelationIssueSummary } from "@paperclipai/shared"; +import type { IssueBlockerAttention, IssueRelationIssueSummary, SuccessfulRunHandoffState } from "@paperclipai/shared"; import { AlertTriangle } from "lucide-react"; +import { Link } from "@/lib/router"; import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb"; import { IssueLinkQuicklook } from "./IssueLinkQuicklook"; @@ -7,12 +8,17 @@ export function IssueBlockedNotice({ issueStatus, blockers, blockerAttention, + successfulRunHandoff, + agentName, }: { issueStatus?: string; blockers: IssueRelationIssueSummary[]; blockerAttention?: IssueBlockerAttention | null; + successfulRunHandoff?: SuccessfulRunHandoffState | null; + agentName?: string | null; }) { - if (blockers.length === 0 && issueStatus !== "blocked") return null; + const showSuccessfulRunHandoff = successfulRunHandoff?.required === true; + if (!showSuccessfulRunHandoff && blockers.length === 0 && issueStatus !== "blocked") return null; const blockerLabel = blockers.length === 1 ? "the linked issue" : "the linked issues"; const terminalBlockers = blockers @@ -61,39 +67,87 @@ export function IssueBlockedNotice({ return (
-

- {blockers.length > 0 - ? isStalled - ? stalledLeafBlockers.length > 1 - ? <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers. - : <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker. - : <>Work on this issue is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage. - : <>Work on this issue is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.} -

- {blockers.length > 0 ? ( -
- {blockers.map(renderBlockerChip)} -
+ {showSuccessfulRunHandoff ? ( + <> +

This issue still needs a next step.

+

+ A run finished successfully, but this issue is still open in{" "} + + in_progress + {" "} + with no clear owner for the next action. +

+
    +
  • Mark it done or cancelled.
  • +
  • Send it for review or ask for input.
  • +
  • Mark it blocked with a blocker owner.
  • +
  • Delegate follow-up work or queue a continuation.
  • +
+
+ {successfulRunHandoff.sourceRunId && successfulRunHandoff.assigneeAgentId ? ( + + run {successfulRunHandoff.sourceRunId.slice(0, 8)} + + ) : successfulRunHandoff.sourceRunId ? ( + + run {successfulRunHandoff.sourceRunId.slice(0, 8)} + + ) : null} + + Corrective wake queued for {agentName ?? "the assignee"} + +
+ {successfulRunHandoff.detectedProgressSummary ? ( +

+ Detected progress: {successfulRunHandoff.detectedProgressSummary} +

+ ) : null} + ) : null} - {showStalledRow ? ( -
- - Stalled in review - - {stalledLeafBlockers.map(renderBlockerChip)} -
- ) : terminalBlockers.length > 0 ? ( -
- - Ultimately waiting on - - {terminalBlockers.map(renderBlockerChip)} -
+ {showSuccessfulRunHandoff && (blockers.length > 0 || issueStatus === "blocked") ? ( +
+ ) : null} + {blockers.length > 0 || issueStatus === "blocked" ? ( + <> +

+ {blockers.length > 0 + ? isStalled + ? stalledLeafBlockers.length > 1 + ? <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers. + : <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker. + : <>Work on this issue is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage. + : <>Work on this issue is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.} +

+ {blockers.length > 0 ? ( +
+ {blockers.map(renderBlockerChip)} +
+ ) : null} + {showStalledRow ? ( +
+ + Stalled in review + + {stalledLeafBlockers.map(renderBlockerChip)} +
+ ) : terminalBlockers.length > 0 ? ( +
+ + Ultimately waiting on + + {terminalBlockers.map(renderBlockerChip)} +
+ ) : null} + ) : null}
diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index dddec3b2cdb..49eed112861 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -836,6 +836,9 @@ describe("IssueChatThread", () => { authorAgentId: "agent-perf-codex", authorUserId: null, body: "Older loaded comment", + authorType: "agent" as const, + presentation: null, + metadata: null, createdAt: new Date("2026-04-06T12:00:00.000Z"), updatedAt: new Date("2026-04-06T12:00:00.000Z"), }; @@ -1056,6 +1059,9 @@ describe("IssueChatThread", () => { authorAgentId: "agent-1", authorUserId: null, body: "Agent summary with **markdown**", + authorType: "agent" as const, + presentation: null, + metadata: null, createdAt: new Date("2026-04-06T12:00:00.000Z"), updatedAt: new Date("2026-04-06T12:00:00.000Z"), }]; @@ -1135,6 +1141,9 @@ describe("IssueChatThread", () => { authorAgentId: null, authorUserId: "local-board", body: "Please continue validation.", + authorType: "user", + presentation: null, + metadata: null, followUpRequested: true, createdAt: new Date("2026-03-11T10:00:00.000Z"), updatedAt: new Date("2026-03-11T10:00:00.000Z"), @@ -1588,6 +1597,9 @@ describe("IssueChatThread", () => { authorAgentId: "agent-1", authorUserId: null, body: "Agent summary", + authorType: "agent", + presentation: null, + metadata: null, createdAt: new Date("2026-04-06T12:00:00.000Z"), updatedAt: new Date("2026-04-06T12:00:00.000Z"), }]} @@ -1624,6 +1636,9 @@ describe("IssueChatThread", () => { authorAgentId: null, authorUserId: "user-1", body: "Need a quick update", + authorType: "user", + presentation: null, + metadata: null, queueState: "queued", queueReason: "hold", createdAt: new Date("2026-04-06T12:00:00.000Z"), @@ -1653,6 +1668,9 @@ describe("IssueChatThread", () => { authorAgentId: null, authorUserId: "user-1", body: "Queue behind active run", + authorType: "user", + presentation: null, + metadata: null, queueState: "queued", queueReason: "active_run", createdAt: new Date("2026-04-06T12:01:00.000Z"), @@ -1997,6 +2015,9 @@ describe("IssueChatThread", () => { authorAgentId: null, authorUserId: "user-1", body: "hello", + authorType: "user", + presentation: null, + metadata: null, createdAt: new Date("2026-04-22T12:00:00.000Z"), updatedAt: new Date("2026-04-22T12:00:00.000Z"), }]} diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 82e50fb3a4d..843f8e59376 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -36,6 +36,7 @@ import type { IssueAttachment, IssueBlockerAttention, IssueRelationIssueSummary, + SuccessfulRunHandoffState, } from "@paperclipai/shared"; import type { ActiveRunForIssue, LiveRunForIssue } from "../api/heartbeats"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; @@ -93,6 +94,16 @@ import { formatAssigneeUserLabel } from "../lib/assignees"; import { useOptionalToastActions } from "../context/ToastContext"; import type { CompanyUserProfile } from "../lib/company-members"; import { timeAgo } from "../lib/timeAgo"; +import { + isSuccessfulRunHandoffComment, + isSuccessfulRunHandoffEscalationComment, +} from "../lib/successful-run-handoff"; +import { SystemNotice } from "./SystemNotice"; +import { buildSystemNoticeProps } from "../lib/system-notice-comment"; +import type { + IssueCommentMetadata, + IssueCommentPresentation, +} from "@paperclipai/shared"; import { describeToolInput, displayToolName, @@ -264,6 +275,7 @@ interface IssueChatThreadProps { activeRun?: ActiveRunForIssue | null; blockedBy?: IssueRelationIssueSummary[]; blockerAttention?: IssueBlockerAttention | null; + successfulRunHandoff?: SuccessfulRunHandoffState | null; companyId?: string | null; projectId?: string | null; issueStatus?: string; @@ -583,6 +595,9 @@ function commentDateLabel(date: Date | string | undefined): string { const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed }: { text: string; recessed?: boolean }) { const { onImageClick } = useContext(IssueChatCtx); + if (isSuccessfulRunHandoffComment(text)) { + return ; + } return ( void; +}) { + const escalated = isSuccessfulRunHandoffEscalationComment(text); + return ( +
+
+ + + {text} + +
+
+ ); +} + function humanizeValue(value: string | null) { if (!value) return "None"; return value.replace(/_/g, " "); @@ -1901,6 +1951,127 @@ function ExpiredRequestConfirmationActivity({ ); } +function isIssueCommentPresentation(value: unknown): value is IssueCommentPresentation { + if (!value || typeof value !== "object") return false; + const v = value as Record; + return v.kind === "system_notice" || v.kind === "message"; +} + +function isIssueCommentMetadata(value: unknown): value is IssueCommentMetadata { + if (!value || typeof value !== "object") return false; + const v = value as Record; + return v.version === 1 && Array.isArray(v.sections); +} + +function SystemNoticeCommentRow({ + message, + anchorId, +}: { + message: ThreadMessage; + anchorId?: string; +}) { + const { onImageClick, agentMap } = useContext(IssueChatCtx); + const custom = message.metadata.custom as Record; + const presentation = isIssueCommentPresentation(custom.presentation) ? custom.presentation : null; + const commentMetadata = isIssueCommentMetadata(custom.commentMetadata) ? custom.commentMetadata : null; + const runAgentId = typeof custom.runAgentId === "string" ? custom.runAgentId : null; + const runId = typeof custom.runId === "string" ? custom.runId : null; + const authorType = typeof custom.authorType === "string" ? custom.authorType : null; + const authorName = typeof custom.authorName === "string" ? custom.authorName : null; + const bodyText = message.content + .filter((p): p is { type: "text"; text: string } => p.type === "text") + .map((p) => p.text) + .join("\n\n"); + const [copied, setCopied] = useState(false); + const [copiedLink, setCopiedLink] = useState(false); + + const source = (() => { + const runAgentName = runAgentId ? agentMap?.get(runAgentId)?.name ?? null : null; + if (authorType === "system") { + const label = runAgentName ?? "Paperclip"; + if (runAgentId && runId) return { label, href: `/agents/${runAgentId}/runs/${runId}` }; + return { label }; + } + if (runAgentId && runId) { + return { label: authorName ?? runAgentName ?? "Paperclip", href: `/agents/${runAgentId}/runs/${runId}` }; + } + if (authorName) return { label: authorName }; + return undefined; + })(); + + const props = buildSystemNoticeProps({ + presentation, + metadata: commentMetadata, + body: ( + + {bodyText} + + ), + timestamp: message.createdAt ? new Date(message.createdAt).toISOString() : undefined, + source, + runAgentId, + }); + + const handleCopy = () => { + void navigator.clipboard.writeText(bodyText).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + const handleCopyLink = () => { + if (!anchorId || typeof window === "undefined") return; + const url = `${window.location.origin}${window.location.pathname}#${anchorId}`; + void navigator.clipboard.writeText(url).then(() => { + setCopiedLink(true); + setTimeout(() => setCopiedLink(false), 2000); + }); + }; + + return ( +
+
+ +
+ + + + {message.createdAt ? commentDateLabel(message.createdAt) : ""} + + + + {message.createdAt ? formatDateTime(message.createdAt) : ""} + + + {anchorId ? ( + + ) : null} + +
+
+
+ ); +} + function IssueChatSystemMessage({ message }: { message: ThreadMessage }) { const { agentMap, @@ -1933,6 +2104,15 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) { ? custom.interaction : null; + if (custom.kind === "system_notice") { + return ( + + ); + } + if (custom.kind === "interaction" && interaction) { if (interaction.kind === "request_confirmation" && interaction.status === "expired") { return ( @@ -3077,6 +3257,7 @@ export function IssueChatThread({ activeRun = null, blockedBy = [], blockerAttention = null, + successfulRunHandoff = null, companyId, projectId, issueStatus, @@ -3700,6 +3881,12 @@ export function IssueChatThread({ issueStatus={issueStatus} blockers={unresolvedBlockers} blockerAttention={blockerAttention} + successfulRunHandoff={successfulRunHandoff} + agentName={ + successfulRunHandoff?.assigneeAgentId + ? agentMap?.get(successfulRunHandoff.assigneeAgentId)?.name ?? null + : null + } />
diff --git a/ui/src/components/IssueChatThreadSystemNotice.test.tsx b/ui/src/components/IssueChatThreadSystemNotice.test.tsx new file mode 100644 index 00000000000..01453d38871 --- /dev/null +++ b/ui/src/components/IssueChatThreadSystemNotice.test.tsx @@ -0,0 +1,398 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import type { ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { MemoryRouter } from "react-router-dom"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { IssueChatThread } from "./IssueChatThread"; +import type { IssueChatComment } from "../lib/issue-chat-messages"; +import type { Agent } from "@paperclipai/shared"; + +vi.mock("@assistant-ui/react", () => ({ + AssistantRuntimeProvider: ({ children }: { children: ReactNode }) =>
{children}
, + useAui: () => ({ thread: () => ({ append: async () => undefined }) }), +})); + +vi.mock("./transcript/useLiveRunTranscripts", () => ({ + useLiveRunTranscripts: () => ({ + transcriptByRun: new Map(), + hasOutputForRun: () => false, + }), +})); + +vi.mock("./MarkdownBody", () => ({ + MarkdownBody: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +vi.mock("./MarkdownEditor", () => ({ + MarkdownEditor: () =>