From e0828cfaf6e27d0ab45061c06535a3b48a9a659f Mon Sep 17 00:00:00 2001 From: "seahaven-openswe[bot]" <296972425+seahaven-openswe[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:28:21 -0400 Subject: [PATCH] chore: cherry-pick deferred reviewer-misc upstream commits (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add PR trace resolution (#1612) * feat: add PR trace resolution Co-authored-by: open-swe[bot] * fix: inject reviewer trace context as JSON Co-authored-by: open-swe[bot] * fix: address review on PR trace resolution Use the documented LangSmith metadata filter syntax (and(eq(metadata_key,...), eq(metadata_value,...))) instead of has(metadata, '{...}'), which does not match runs — _list_thread_runs was silently returning nothing. Bound full-text searches to a 90-day window so they don't hit LangSmith's large-window rate limit. Also folds in the best-effort branch->head-sha resolver (dropping the weighted scoring/threshold + repo/file evidence + GitHub hydration), sandbox JSON injection, and the admin "Resolve trace" dry-run endpoint. The IDOR findings are moot: resolve_pr_to_threads/summarize_agent_session were removed; resolution now runs deterministically from the trusted run config with no model-controlled pr_url or thread_id. * fix: scope branch trace search to the repo Branch names like fix-tests aren't unique across repos (or older PRs) in a shared tracing project, so an unscoped branch hit could resolve to an unrelated thread and write its runs into the reviewer sandbox. Require the repo slug to co-occur with the branch in matched runs; the full head SHA stays unscoped since it is globally unique. Addresses open-swe review on PR #1612. --------- Co-authored-by: open-swe[bot] (cherry picked from commit 69148f54f54f29ccf340de53a2c7932eea739def) * fix: post reviewer resolution notes verbatim (#1624) * fix: post reviewer resolution notes verbatim Co-authored-by: open-swe[bot] * fix: stabilize dashboard follow-up e2e Co-authored-by: open-swe[bot] * fix: preserve dashboard attribution in e2e Co-authored-by: open-swe[bot] * fix: make e2e attribution marker durable Co-authored-by: open-swe[bot] * fix: only echo found e2e attribution Co-authored-by: open-swe[bot] * fix: check live dashboard attribution in e2e Co-authored-by: open-swe[bot] --------- Co-authored-by: open-swe[bot] (cherry picked from commit 5da3d0c657e355add88fe12b5109c0bd6b47a121) * chore: opt-in tracemalloc to attribute unclosed aiohttp sessions (#1657) Prod logs show bursts of 'Unclosed client session' (aiohttp), leaking fds + memory, but the warning omits the allocation site. When DEBUG_TRACEMALLOC is set, start tracemalloc at webapp import so aiohttp appends an 'Object allocated at' traceback naming the exact source. Inert when the env var is unset. Co-authored-by: open-swe[bot] (cherry picked from commit 320bb39ab1f5cd7a2acdac52c7b375854334176c) * feat: add PR review link route (#1698) * feat: add PR review link route Co-authored-by: open-swe[bot] * fix: avoid duplicate review shortcut runs Co-authored-by: open-swe[bot] --------- Co-authored-by: open-swe[bot] (cherry picked from commit 52fe29168814d7936ee6612b9c81f33339bb05b0) * style: clean up leftover blank lines from cherry-pick conflict resolution * fix(e2e): drop duplicate _ATTRIBUTION_RE from cherry-pick The reviewer-misc pick re-added _ATTRIBUTION_RE next to _latest_attribution, but the constant was already defined at module top (line 57, alongside _PLAN_URL_RE) via the earlier #81 sync. Remove the redundant redefinition; _latest_attribution resolves the surviving top-level constant. --------- Co-authored-by: Johannes du Plessis Co-authored-by: open-swe[bot] Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com> Co-authored-by: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa --- agent/dashboard/team_settings.py | 1 - agent/webapp.py | 20 ++ ui/src/routeTree.gen.ts | 21 ++ ui/src/routes/$owner.$repo.pull.$number.tsx | 211 ++++++++++++++++++++ 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 ui/src/routes/$owner.$repo.pull.$number.tsx diff --git a/agent/dashboard/team_settings.py b/agent/dashboard/team_settings.py index bca2ab85..e81ff4b8 100644 --- a/agent/dashboard/team_settings.py +++ b/agent/dashboard/team_settings.py @@ -31,7 +31,6 @@ TEAM_SETTINGS_KEY = "default" # prompt. Generous enough for a detailed policy, small enough to stay bounded. ORG_GUIDELINES_MAX_CHARS = 10_000 REVIEW_TRACING_PROJECT_MAX_CHARS = 256 - # Sea Haven review baseline seeded as the org-wide guidelines default. Surfaces # in the reviewer prompt for every repo until an admin overrides it with a # non-empty value via the dashboard (PUT /team-settings). Keep it stack-agnostic diff --git a/agent/webapp.py b/agent/webapp.py index da19a7af..c47fd231 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -131,6 +131,26 @@ from .utils.thread_ids import generate_thread_id_from_slack_thread logger = logging.getLogger(__name__) +# Opt-in leak diagnostics. Bursts of aiohttp "Unclosed client session" warnings +# (from a third-party SDK) leak fds + memory in prod, but the warning omits the +# allocation site. With tracemalloc running, aiohttp appends an "Object allocated +# at" traceback to each warning, naming the exact source. Inert unless the env +# var is set, so this is safe to ship and flip on for one diagnostic run. +if os.environ.get("DEBUG_TRACEMALLOC"): + import tracemalloc + + try: + _tracemalloc_frames = int(os.environ.get("DEBUG_TRACEMALLOC_FRAMES") or "25") + except ValueError: + _tracemalloc_frames = 25 + tracemalloc.start(_tracemalloc_frames) + logger.warning( + "DEBUG_TRACEMALLOC enabled: tracemalloc started (%d frames) to attribute " + "unclosed-session warnings", + _tracemalloc_frames, + ) + + @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: from .utils.model import validate_local_dev_llm_config diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 4a12a8e1..02540c57 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -31,6 +31,7 @@ import { Route as ReviewRepositoriesOwnerRouteImport } from './routes/review_.re import { Route as AgentsAutomationsNewRouteImport } from './routes/agents/automations/new' import { Route as AgentsAutomationsScheduleIdRouteImport } from './routes/agents/automations/$scheduleId' import { Route as AgentsThreadIdPlanRouteImport } from './routes/agents/$threadId_.plan' +import { Route as OwnerRepoPullNumberRouteImport } from './routes/$owner.$repo.pull.$number' import { Route as AgentsReviewsOwnerRepoNumberRouteImport } from './routes/agents/reviews/$owner.$repo.$number' const UsageRoute = UsageRouteImport.update({ @@ -144,6 +145,11 @@ const AgentsThreadIdPlanRoute = AgentsThreadIdPlanRouteImport.update({ path: '/$threadId/plan', getParentRoute: () => AgentsRoute, } as any) +const OwnerRepoPullNumberRoute = OwnerRepoPullNumberRouteImport.update({ + id: '/$owner/$repo/pull/$number', + path: '/$owner/$repo/pull/$number', + getParentRoute: () => rootRouteImport, +} as any) const AgentsReviewsOwnerRepoNumberRoute = AgentsReviewsOwnerRepoNumberRouteImport.update({ id: '/reviews/$owner/$repo/$number', @@ -174,6 +180,7 @@ export interface FileRoutesByFullPath { '/review/repositories/$owner': typeof ReviewRepositoriesOwnerRoute '/agents/automations/': typeof AgentsAutomationsIndexRoute '/agents/reviews/': typeof AgentsReviewsIndexRoute + '/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute '/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute } export interface FileRoutesByTo { @@ -198,6 +205,7 @@ export interface FileRoutesByTo { '/review/repositories/$owner': typeof ReviewRepositoriesOwnerRoute '/agents/automations': typeof AgentsAutomationsIndexRoute '/agents/reviews': typeof AgentsReviewsIndexRoute + '/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute '/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute } export interface FileRoutesById { @@ -224,6 +232,7 @@ export interface FileRoutesById { '/review_/repositories/$owner': typeof ReviewRepositoriesOwnerRoute '/agents/automations/': typeof AgentsAutomationsIndexRoute '/agents/reviews/': typeof AgentsReviewsIndexRoute + '/$owner/$repo/pull/$number': typeof OwnerRepoPullNumberRoute '/agents/reviews/$owner/$repo/$number': typeof AgentsReviewsOwnerRepoNumberRoute } export interface FileRouteTypes { @@ -251,6 +260,7 @@ export interface FileRouteTypes { | '/review/repositories/$owner' | '/agents/automations/' | '/agents/reviews/' + | '/$owner/$repo/pull/$number' | '/agents/reviews/$owner/$repo/$number' fileRoutesByTo: FileRoutesByTo to: @@ -275,6 +285,7 @@ export interface FileRouteTypes { | '/review/repositories/$owner' | '/agents/automations' | '/agents/reviews' + | '/$owner/$repo/pull/$number' | '/agents/reviews/$owner/$repo/$number' id: | '__root__' @@ -300,6 +311,7 @@ export interface FileRouteTypes { | '/review_/repositories/$owner' | '/agents/automations/' | '/agents/reviews/' + | '/$owner/$repo/pull/$number' | '/agents/reviews/$owner/$repo/$number' fileRoutesById: FileRoutesById } @@ -318,6 +330,7 @@ export interface RootRouteChildren { AgentsSnapshotsRoute: typeof AgentsSnapshotsRoute ReviewStylesRoute: typeof ReviewStylesRoute ReviewRepositoriesOwnerRoute: typeof ReviewRepositoriesOwnerRoute + OwnerRepoPullNumberRoute: typeof OwnerRepoPullNumberRoute } declare module '@tanstack/react-router' { @@ -476,6 +489,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AgentsThreadIdPlanRouteImport parentRoute: typeof AgentsRoute } + '/$owner/$repo/pull/$number': { + id: '/$owner/$repo/pull/$number' + path: '/$owner/$repo/pull/$number' + fullPath: '/$owner/$repo/pull/$number' + preLoaderRoute: typeof OwnerRepoPullNumberRouteImport + parentRoute: typeof rootRouteImport + } '/agents/reviews/$owner/$repo/$number': { id: '/agents/reviews/$owner/$repo/$number' path: '/reviews/$owner/$repo/$number' @@ -528,6 +548,7 @@ const rootRouteChildren: RootRouteChildren = { AgentsSnapshotsRoute: AgentsSnapshotsRoute, ReviewStylesRoute: ReviewStylesRoute, ReviewRepositoriesOwnerRoute: ReviewRepositoriesOwnerRoute, + OwnerRepoPullNumberRoute: OwnerRepoPullNumberRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/ui/src/routes/$owner.$repo.pull.$number.tsx b/ui/src/routes/$owner.$repo.pull.$number.tsx new file mode 100644 index 00000000..0c816a56 --- /dev/null +++ b/ui/src/routes/$owner.$repo.pull.$number.tsx @@ -0,0 +1,211 @@ +import { Link, Navigate, createFileRoute } from "@tanstack/react-router" +import { useEffect, useMemo, useRef } from "react" +import { ArrowSquareOutIcon, GitPullRequestIcon } from "@phosphor-icons/react" +import { useMutation, useQuery } from "@tanstack/react-query" + +import { buttonVariants } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" +import { api } from "@/lib/api" +import { RequireLogin } from "@/lib/auth-redirect" +import { useSession } from "@/lib/session" +import { cn } from "@/lib/utils" + +export const Route = createFileRoute("/$owner/$repo/pull/$number")({ + component: PullRequestReviewLinkPage, +}) + +function PullRequestReviewLinkPage() { + const { owner, repo, number } = Route.useParams() + const prNumber = Number(number) + const session = useSession() + const stableReviewPath = `/agents/reviews/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${prNumber}` + const githubPrUrl = useMemo( + () => `https://github.com/${owner}/${repo}/pull/${number}`, + [owner, repo, number] + ) + const existingReview = useQuery({ + queryKey: ["review", owner, repo, prNumber], + queryFn: () => api.getReview(owner, repo, prNumber), + enabled: !!session.data && Number.isFinite(prNumber), + retry: false, + }) + const triggerRef = useRef(null) + const triggerReview = useMutation({ + mutationFn: () => api.reReview(owner, repo, prNumber), + }) + + useEffect(() => { + if (!session.data || !Number.isFinite(prNumber)) return + if (existingReview.isLoading || existingReview.data?.status === "running") { + return + } + const key = `${owner}/${repo}#${prNumber}` + if (triggerRef.current === key) return + triggerRef.current = key + triggerReview.mutate() + }, [ + existingReview.data?.status, + existingReview.isLoading, + owner, + repo, + prNumber, + session.data, + triggerReview, + ]) + + if (session.isLoading) { + return ( +
+ +
+ ) + } + + if (!session.data) return + + if (!Number.isFinite(prNumber)) { + return ( + + ) + } + + if (existingReview.data?.status === "running" || triggerReview.isSuccess) { + return ( + + ) + } + + if (triggerReview.isError) { + return ( + triggerReview.mutate()} + /> + ) + } + + const isCheckingExistingReview = existingReview.isLoading + + return ( + + ) +} + +function ReviewLinkCard({ + title, + description, + owner, + repo, + number, + githubPrUrl, + stableReviewPath, + loading = false, + onRetry, +}: { + title: string + description: string + owner: string + repo: string + number: string + githubPrUrl: string + stableReviewPath?: string + loading?: boolean + onRetry?: () => void +}) { + return ( +
+ + + + + {title} + + {description} + + +
+
+ {owner}/{repo} #{number} +
+ + View on GitHub + + +
+ {loading && } +
+ + {onRetry && ( + + )} + {stableReviewPath && ( + + Open stable review page + + )} + + Open GitHub PR + + +
+
+ ) +}