From 7b334ff2b785dfd11578c3e33ecad55d501e76ea Mon Sep 17 00:00:00 2001 From: Numman Ali Date: Wed, 4 Mar 2026 22:28:22 +0000 Subject: [PATCH 1/2] fix: exclude terminated agents from list and org chart endpoints Terminated agents (e.g. from rejected hire approvals) were visible in GET /companies/:companyId/agents and GET /companies/:companyId/org because list() and orgForCompany() had no status filtering. - Add ne(agents.status, "terminated") filter to both queries - Add optional { includeTerminated: true } param to list() for callers that need all agents (e.g. company-portability export with skip counting) - orgForCompany() always excludes terminated (no escape hatch needed) Fixes #5 --- server/src/services/agents.ts | 15 +++++++++++---- server/src/services/company-portability.ts | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index 54d48045..703a3f95 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -1,5 +1,5 @@ import { createHash, randomBytes } from "node:crypto"; -import { and, desc, eq, inArray } from "drizzle-orm"; +import { and, desc, eq, inArray, ne } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, @@ -251,8 +251,12 @@ export function agentService(db: Db) { } return { - list: async (companyId: string) => { - const rows = await db.select().from(agents).where(eq(agents.companyId, companyId)); + list: async (companyId: string, options?: { includeTerminated?: boolean }) => { + const conditions = [eq(agents.companyId, companyId)]; + if (!options?.includeTerminated) { + conditions.push(ne(agents.status, "terminated")); + } + const rows = await db.select().from(agents).where(and(...conditions)); return rows.map(normalizeAgentRow); }, @@ -469,7 +473,10 @@ export function agentService(db: Db) { }, orgForCompany: async (companyId: string) => { - const rows = await db.select().from(agents).where(eq(agents.companyId, companyId)); + const rows = await db + .select() + .from(agents) + .where(and(eq(agents.companyId, companyId), ne(agents.status, "terminated"))); const normalizedRows = rows.map(normalizeAgentRow); const byManager = new Map(); for (const row of normalizedRows) { diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 84575881..a80a334a 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -556,7 +556,7 @@ export function companyPortabilityService(db: Db) { requiredSecrets: [], }; - const allAgentRows = include.agents ? await agents.list(companyId) : []; + const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated"); if (include.agents) { const skipped = allAgentRows.length - agentRows.length; From bdc0480e621ca4975b7de0db405cbc1b67170e0e Mon Sep 17 00:00:00 2001 From: Tyler Wince Date: Wed, 4 Mar 2026 20:20:21 -0700 Subject: [PATCH 2/2] fix(ui): render sub-goals in goal detail tree --- ui/src/components/GoalTree.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/components/GoalTree.tsx b/ui/src/components/GoalTree.tsx index fe20ee64..116b1668 100644 --- a/ui/src/components/GoalTree.tsx +++ b/ui/src/components/GoalTree.tsx @@ -92,7 +92,8 @@ function GoalNode({ goal, children, allGoals, depth, goalLink, onSelect }: GoalN } export function GoalTree({ goals, goalLink, onSelect }: GoalTreeProps) { - const roots = goals.filter((g) => !g.parentId); + const goalIds = new Set(goals.map((g) => g.id)); + const roots = goals.filter((g) => !g.parentId || !goalIds.has(g.parentId)); if (goals.length === 0) { return

No goals.

;