From 2719f31e3f9ef0f6285ec9c61a42cc0e899fdb7d Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 5 Jun 2026 19:19:38 +0000 Subject: [PATCH] fix: optimize low-trust blocked count Co-Authored-By: Paperclip --- .../low-trust-red-team-routes.test.ts | 35 +++++++++++++++++ server/src/routes/issues.ts | 18 +++++++++ server/src/services/heartbeat.ts | 2 +- server/src/services/issues.ts | 39 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/low-trust-red-team-routes.test.ts b/server/src/__tests__/low-trust-red-team-routes.test.ts index a873892aa9b..d97a84005b6 100644 --- a/server/src/__tests__/low-trust-red-team-routes.test.ts +++ b/server/src/__tests__/low-trust-red-team-routes.test.ts @@ -772,6 +772,41 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () => } }); + it("counts blocked inbox issues with the low-trust boundary applied in the database", async () => { + const fixture = await seedLowTrustFixture(db); + await db.insert(issues).values([ + { + companyId: fixture.company.id, + projectId: fixture.projects.allowed.id, + parentId: fixture.issues.reviewRoot.id, + title: "Visible blocked vendor wait", + status: "blocked", + priority: "medium", + description: "external owner: Visible vendor\nexternal action: Finish visible review", + }, + { + companyId: fixture.company.id, + projectId: fixture.projects.outOfScope.id, + title: "Hidden blocked vendor wait", + status: "blocked", + priority: "medium", + description: "external owner: Hidden vendor\nexternal action: Finish hidden review", + }, + ]); + + const boardCount = await request(createApp(db, boardActor(fixture))) + .get(`/api/companies/${fixture.company.id}/issues/count`) + .query({ attention: "blocked", q: "blocked vendor wait" }); + expect(boardCount.status, JSON.stringify(boardCount.body)).toBe(200); + expect(boardCount.body.count).toBe(2); + + const lowTrustCount = await request(createApp(db, agentActor(fixture))) + .get(`/api/companies/${fixture.company.id}/issues/count`) + .query({ attention: "blocked", q: "blocked vendor wait" }); + expect(lowTrustCount.status, JSON.stringify(lowTrustCount.body)).toBe(200); + expect(lowTrustCount.body.count).toBe(1); + }); + it("redacts quarantined low-trust output from higher-trust wake and continuation contexts", async () => { const fixture = await seedLowTrustFixture(db); const lowTrustApp = createApp(db, agentActor(fixture)); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 8fbe3765af6..1136ca15587 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -2440,6 +2440,24 @@ export function issueRoutes( } as const; if (!(await actorCanReadCompanyScope(req, companyId))) { + const trustResolution = req.actor.type === "agent" + ? await resolveAgentTrustForIssue({ + agentId: req.actor.agentId, + runId: req.actor.runId, + }, companyId, null) + : null; + if (trustResolution?.kind === "denied") { + throw forbidden(trustResolution.detail); + } + if (trustResolution?.kind === "low_trust_review") { + const count = await svc.count(companyId, { + ...blockedCountFilters, + lowTrustBoundary: trustResolution.boundary, + }); + res.json({ count }); + return; + } + let offset = 0; let visibleCount = 0; while (true) { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 56406a7c3a3..9e72e6fbcc2 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -7552,7 +7552,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) db, companyId: agent.companyId, contextSnapshot: context, - continuationSummary: safeContinuationSummary, + continuationSummary, issueSummary: issueRef ? { id: issueRef.id, diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 3a7c26816a5..e7dd67f0ceb 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -44,6 +44,7 @@ import type { IssueProductivityReview, IssueProductivityReviewTrigger, IssueRelationIssueSummary, + LowTrustBoundary, SuccessfulRunHandoffState, } from "@paperclipai/shared"; import { @@ -244,6 +245,7 @@ export interface IssueFilters { includeBlockedBy?: boolean; includeBlockedInboxAttention?: boolean; hasPlanDocument?: boolean; + lowTrustBoundary?: LowTrustBoundary & { companyId: string }; q?: string; limit?: number; offset?: number; @@ -1181,6 +1183,39 @@ const PRODUCTIVITY_REVIEW_TRIGGERS: readonly IssueProductivityReviewTrigger[] = "long_active_duration", "high_churn", ]; + +function lowTrustBoundaryIssueCondition( + companyId: string, + boundary: (LowTrustBoundary & { companyId: string }) | null | undefined, +) { + if (!boundary || boundary.companyId !== companyId) return null; + const clauses: SQL[] = []; + const issueIds = [...new Set(boundary.issueIds ?? [])]; + const projectIds = [...new Set(boundary.projectIds ?? [])]; + if (issueIds.length > 0) clauses.push(inArray(issues.id, issueIds)); + if (projectIds.length > 0) clauses.push(inArray(issues.projectId, projectIds)); + if (boundary.rootIssueId) { + clauses.push(sql` + ${issues.id} IN ( + WITH RECURSIVE descendants(id) AS ( + SELECT ${issues.id} + FROM ${issues} + WHERE ${issues.companyId} = ${companyId} + AND ${issues.id} = ${boundary.rootIssueId} + UNION + SELECT ${issues.id} + FROM ${issues} + JOIN descendants ON ${issues.parentId} = descendants.id + WHERE ${issues.companyId} = ${companyId} + ) + SELECT id FROM descendants + ) + `); + } + if (clauses.length === 0) return sql`false`; + return or(...clauses); +} + const BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES = ["done", "cancelled"]; const BLOCKER_ATTENTION_MAX_DEPTH = 8; const BLOCKER_ATTENTION_MAX_NODES = 2000; @@ -2874,6 +2909,8 @@ async function blockedInboxIssueConditions( ) `); } + const lowTrustCondition = lowTrustBoundaryIssueCondition(companyId, filters?.lowTrustBoundary); + if (lowTrustCondition) conditions.push(lowTrustCondition); if (filters?.status) { const statuses = filters.status.split(",").map((status) => status.trim()).filter(Boolean); if (statuses.length > 0) { @@ -3841,6 +3878,8 @@ export function issueService(db: Db) { ) `); } + const lowTrustCondition = lowTrustBoundaryIssueCondition(companyId, filters?.lowTrustBoundary); + if (lowTrustCondition) conditions.push(lowTrustCondition); if (filters?.status) { const statuses = filters.status.split(",").map((s) => s.trim()); conditions.push(statuses.length === 1 ? eq(issues.status, statuses[0]) : inArray(issues.status, statuses));