Fix stale issue live-run state

This commit is contained in:
Dotta
2026-04-12 20:41:31 -05:00
parent 2172476e84
commit ab5eeca94e
3 changed files with 65 additions and 2 deletions

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import type { Issue } from "@paperclipai/shared";
import type { ActiveRunForIssue } from "../api/heartbeats";
import { resolveIssueActiveRun, shouldTrackIssueActiveRun } from "./issueActiveRun";
describe("issueActiveRun", () => {
const makeIssue = (
overrides: Partial<Pick<Issue, "status" | "executionRunId">>,
): Pick<Issue, "status" | "executionRunId"> => ({
status: "todo",
executionRunId: null,
...overrides,
});
it("tracks active runs while an issue is still in progress", () => {
expect(shouldTrackIssueActiveRun(makeIssue({ status: "in_progress" }))).toBe(true);
});
it("tracks active runs while an execution run id is still attached", () => {
expect(shouldTrackIssueActiveRun(makeIssue({ status: "done", executionRunId: "run-123" }))).toBe(true);
});
it("drops stale cached active runs once the issue is closed and unlocked", () => {
const staleActiveRun: ActiveRunForIssue = {
id: "run-123",
status: "running",
invocationSource: "assignment",
triggerDetail: "system",
startedAt: "2026-04-13T01:29:00.000Z",
finishedAt: null,
createdAt: "2026-04-13T01:29:00.000Z",
agentId: "agent-1",
agentName: "Builder",
adapterType: "codex_local",
issueId: "issue-1",
};
expect(
resolveIssueActiveRun(
makeIssue({ status: "done" }),
staleActiveRun,
),
).toBeNull();
});
});

View File

@@ -0,0 +1,15 @@
import type { Issue } from "@paperclipai/shared";
import type { ActiveRunForIssue } from "../api/heartbeats";
export function shouldTrackIssueActiveRun(
issue: Pick<Issue, "status" | "executionRunId"> | null | undefined,
): boolean {
return Boolean(issue && (issue.status === "in_progress" || issue.executionRunId));
}
export function resolveIssueActiveRun(
issue: Pick<Issue, "status" | "executionRunId"> | null | undefined,
activeRun: ActiveRunForIssue | null | undefined,
): ActiveRunForIssue | null {
return shouldTrackIssueActiveRun(issue) ? (activeRun ?? null) : null;
}

View File

@@ -26,6 +26,7 @@ import {
readIssueDetailHeaderSeed,
rememberIssueDetailLocationState,
} from "../lib/issueDetailBreadcrumb";
import { resolveIssueActiveRun, shouldTrackIssueActiveRun } from "../lib/issueActiveRun";
import {
hasBlockingShortcutDialog,
resolveIssueDetailGoKeyAction,
@@ -471,13 +472,15 @@ export function IssueDetail() {
placeholderData: keepPreviousData,
});
const { data: activeRun, isLoading: activeRunLoading } = useQuery({
const shouldPollActiveRun = shouldTrackIssueActiveRun(issue);
const { data: rawActiveRun, isLoading: activeRunLoading } = useQuery({
queryKey: queryKeys.issues.activeRun(issueId!),
queryFn: () => heartbeatsApi.activeRunForIssue(issueId!),
enabled: !!issueId && (!!issue?.executionRunId || issue?.status === "in_progress"),
enabled: !!issueId && shouldPollActiveRun,
refetchInterval: (liveRuns?.length ?? 0) > 0 ? false : 3000,
placeholderData: keepPreviousData,
});
const activeRun = resolveIssueActiveRun(issue, rawActiveRun);
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
const runningIssueRun = useMemo(