From 973e8f5e980d2ceffd0e979d5bab28e7aca8de99 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 10 Jun 2026 20:20:49 +0000 Subject: [PATCH] Address Greptile review on aggregation PR Push the review-stage caller filter into SQL so agent callers in busy companies can't get a silently truncated review feed, make the routine reference check a targeted JSONB query instead of a company-wide stage scan, and expose connections on the UI PipelineListItem type. Co-Authored-By: Paperclip --- server/src/services/pipelines-aggregation.ts | 13 ++++++------- server/src/services/pipelines.ts | 17 ++++++++++------- ui/src/api/pipelines.ts | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/server/src/services/pipelines-aggregation.ts b/server/src/services/pipelines-aggregation.ts index 83d590f25f8..c6f2d878cba 100644 --- a/server/src/services/pipelines-aggregation.ts +++ b/server/src/services/pipelines-aggregation.ts @@ -56,10 +56,10 @@ function caseDisplay(row: { case: CaseRow; stage: StageRow; pipeline: PipelineRo // Current review-stage approver semantics: reviewerKind "human" awaits any // human caller, "any" awaits anyone. B1 (stage requireApproval + named // approver) extends this predicate when it lands — keep the seam here. -function reviewStageAwaitsCaller(config: Record, caller: AttentionCaller) { - const reviewerKind = typeof config.reviewerKind === "string" ? config.reviewerKind : "human"; - if (reviewerKind === "any") return true; - return caller.type === "user"; +// SQL-side so busy companies can't truncate an agent's review feed. +function reviewStageAwaitsCallerSql(caller: AttentionCaller) { + if (caller.type === "user") return sql`true`; + return sql`coalesce(${pipelineStages.config}->>'reviewerKind', 'human') = 'any'`; } function boundedLimit(limit: number | undefined, fallback: number, max: number) { @@ -129,13 +129,12 @@ export async function listPipelineAttention( eq(pipelines.companyId, input.companyId), eq(pipelineStages.kind, "review"), isNull(pipelineCases.terminalKind), + reviewStageAwaitsCallerSql(input.caller), )) .orderBy(asc(pipelineCases.createdAt)) - .limit(PIPELINE_ATTENTION_MAX_LIMIT * 2); + .limit(limit); const reviews = reviewRows - .filter((row) => reviewStageAwaitsCaller((row.stage.config ?? {}) as Record, input.caller)) - .slice(0, limit) .map((row) => { const config = (row.stage.config ?? {}) as Record; return { diff --git a/server/src/services/pipelines.ts b/server/src/services/pipelines.ts index e0116e319d2..b03698d1669 100644 --- a/server/src/services/pipelines.ts +++ b/server/src/services/pipelines.ts @@ -853,15 +853,18 @@ export function pipelineService(db: Db, deps: { heartbeat?: IssueAssignmentWakeu dbOrTx: PipelineDb, input: { companyId: string; routineId: string; exceptStageId?: string | null }, ) { - const stages = await dbOrTx - .select({ id: pipelineStages.id, config: pipelineStages.config }) + const referencing = await dbOrTx + .select({ id: pipelineStages.id }) .from(pipelineStages) .innerJoin(pipelines, eq(pipelineStages.pipelineId, pipelines.id)) - .where(eq(pipelines.companyId, input.companyId)); - return stages.some((stage) => - stage.id !== input.exceptStageId && - stageAutomationRoutineIdFromConfig((stage.config ?? {}) as PipelineStageConfig) === input.routineId - ); + .where(and( + eq(pipelines.companyId, input.companyId), + sql`${pipelineStages.config}->'onEnter'->>'type' = 'run_routine'`, + sql`${pipelineStages.config}->'onEnter'->>'routineId' = ${input.routineId}`, + input.exceptStageId ? ne(pipelineStages.id, input.exceptStageId) : undefined, + )) + .limit(1); + return referencing.length > 0; } async function clearPipelineAutomationRoutineIfUnreferenced( diff --git a/ui/src/api/pipelines.ts b/ui/src/api/pipelines.ts index 9c09f658e36..2ad6fe861b2 100644 --- a/ui/src/api/pipelines.ts +++ b/ui/src/api/pipelines.ts @@ -11,6 +11,7 @@ export interface PipelineListItem { archivedAt: Date | string | null; stageCount: number; openCaseCount: number; + connections: { upstreamPipelineIds: string[]; downstreamPipelineIds: string[] }; createdAt: Date | string; updatedAt: Date | string; }