import { useMemo } from "react"; import type { Issue, Agent } from "@paperclipai/shared"; import { useQuery } from "@tanstack/react-query"; import { Link } from "@/lib/router"; import { activityApi, type RunForIssue, type RunLivenessState } from "../api/activity"; import { heartbeatsApi, type ActiveRunForIssue, type LiveRunForIssue } from "../api/heartbeats"; import { cn, relativeTime } from "../lib/utils"; import { queryKeys } from "../lib/queryKeys"; import { keepPreviousDataForSameQueryTail } from "../lib/query-placeholder-data"; type IssueRunLedgerProps = { issueId: string; issueStatus: Issue["status"]; childIssues: Issue[]; agentMap: ReadonlyMap; hasLiveRuns: boolean; }; type IssueRunLedgerContentProps = { runs: RunForIssue[]; liveRuns?: LiveRunForIssue[]; activeRun?: ActiveRunForIssue | null; issueStatus: Issue["status"]; childIssues: Issue[]; agentMap: ReadonlyMap>; }; type LedgerRun = RunForIssue & { isLive?: boolean; agentName?: string; }; type LivenessCopy = { label: string; tone: string; description: string; }; const LIVENESS_COPY: Record = { completed: { label: "Completed", tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", description: "Issue reached a terminal state.", }, advanced: { label: "Advanced", tone: "border-cyan-500/30 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300", description: "Run produced concrete evidence of progress.", }, plan_only: { label: "Plan only", tone: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", description: "Run described future work without concrete action evidence.", }, empty_response: { label: "Empty response", tone: "border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-300", description: "Run finished without useful output.", }, blocked: { label: "Blocked", tone: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300", description: "Run or issue declared a blocker.", }, failed: { label: "Failed", tone: "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300", description: "Run ended unsuccessfully.", }, needs_followup: { label: "Needs follow-up", tone: "border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300", description: "Run produced useful output but did not prove concrete progress.", }, }; const PENDING_LIVENESS_COPY: LivenessCopy = { label: "Checks after finish", tone: "border-border bg-background text-muted-foreground", description: "Liveness is evaluated after the run finishes.", }; const MISSING_LIVENESS_COPY: LivenessCopy = { label: "No liveness data", tone: "border-border bg-background text-muted-foreground", description: "This run has no persisted liveness classification.", }; const TERMINAL_CHILD_STATUSES = new Set(["done", "cancelled"]); const ACTIVE_RUN_STATUSES = new Set(["queued", "running"]); function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } function readString(value: unknown) { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function readNumber(value: unknown) { return typeof value === "number" && Number.isFinite(value) ? value : null; } function formatDuration(start: string | Date | null | undefined, end: string | Date | null | undefined) { if (!start) return null; const startMs = new Date(start).getTime(); const endMs = end ? new Date(end).getTime() : Date.now(); if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null; const totalSeconds = Math.max(0, Math.round((endMs - startMs) / 1000)); if (totalSeconds < 60) return `${totalSeconds}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; if (minutes < 60) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`; } function toIsoString(value: string | Date | null | undefined) { if (!value) return null; return value instanceof Date ? value.toISOString() : value; } function liveRunToLedgerRun(run: LiveRunForIssue | ActiveRunForIssue): LedgerRun { return { runId: run.id, status: run.status, agentId: run.agentId, agentName: run.agentName, adapterType: run.adapterType, startedAt: toIsoString(run.startedAt), finishedAt: toIsoString(run.finishedAt), createdAt: toIsoString(run.createdAt) ?? new Date().toISOString(), invocationSource: run.invocationSource, usageJson: null, resultJson: null, isLive: run.status === "queued" || run.status === "running", }; } function mergeRuns( runs: RunForIssue[], liveRuns: LiveRunForIssue[] | undefined, activeRun: ActiveRunForIssue | null | undefined, ) { const byId = new Map(); for (const run of runs) byId.set(run.runId, run); for (const run of liveRuns ?? []) { const existing = byId.get(run.id); byId.set(run.id, existing ? { ...existing, isLive: true, agentName: run.agentName } : liveRunToLedgerRun(run)); } if (activeRun && !byId.has(activeRun.id)) { byId.set(activeRun.id, liveRunToLedgerRun(activeRun)); } return [...byId.values()].sort((a, b) => { const aTime = new Date(a.startedAt ?? a.createdAt).getTime(); const bTime = new Date(b.startedAt ?? b.createdAt).getTime(); if (aTime !== bTime) return bTime - aTime; return b.runId.localeCompare(a.runId); }); } function statusLabel(status: string) { return status.replace(/_/g, " "); } function isActiveRun(run: Pick) { return run.isLive || ACTIVE_RUN_STATUSES.has(run.status); } function runSummary(run: LedgerRun, agentMap: ReadonlyMap>) { const agentName = compactAgentName(run, agentMap); if (run.status === "running") return `Running now by ${agentName}`; if (run.status === "queued") return `Queued for ${agentName}`; return `${statusLabel(run.status)} by ${agentName}`; } function livenessCopyForRun(run: LedgerRun) { if (run.livenessState) return LIVENESS_COPY[run.livenessState]; return isActiveRun(run) ? PENDING_LIVENESS_COPY : MISSING_LIVENESS_COPY; } function stopReasonLabel(run: RunForIssue) { const result = asRecord(run.resultJson); const stopReason = readString(result?.stopReason); const timeoutFired = result?.timeoutFired === true; const effectiveTimeoutSec = readNumber(result?.effectiveTimeoutSec); const timeoutText = effectiveTimeoutSec && effectiveTimeoutSec > 0 ? `${effectiveTimeoutSec}s timeout` : null; if (timeoutFired || stopReason === "timeout") { return timeoutText ? `timeout (${timeoutText})` : "timeout"; } if (stopReason === "budget_paused") return "budget paused"; if (stopReason === "cancelled") return "cancelled"; if (stopReason === "paused") return "paused"; if (stopReason === "process_lost") return "process lost"; if (stopReason === "adapter_failed") return "adapter failed"; if (stopReason === "completed") return timeoutText ? `completed (${timeoutText})` : "completed"; return timeoutText; } function stopStatusLabel(run: LedgerRun, stopReason: string | null) { if (stopReason) return stopReason; if (run.status === "queued") return "Waiting to start"; if (run.status === "running") return "Still running"; if (!run.livenessState) return "Unavailable"; return "No stop reason"; } function lastUsefulActionLabel(run: LedgerRun) { if (run.lastUsefulActionAt) return relativeTime(run.lastUsefulActionAt); if (isActiveRun(run)) return "No action recorded yet"; if (run.livenessState === "plan_only" || run.livenessState === "needs_followup") { return "No concrete action"; } if (run.livenessState === "empty_response") return "No useful output"; if (!run.livenessState) return "Unavailable"; return "None recorded"; } function continuationLabel(run: LedgerRun) { if (!run.continuationAttempt || run.continuationAttempt <= 0) return null; return `Continuation attempt ${run.continuationAttempt}`; } function hasExhaustedContinuation(run: RunForIssue) { return /continuation attempts exhausted/i.test(run.livenessReason ?? ""); } function childIssueSummary(childIssues: Issue[]) { const active = childIssues.filter((issue) => !TERMINAL_CHILD_STATUSES.has(issue.status)); const done = childIssues.filter((issue) => issue.status === "done").length; const cancelled = childIssues.filter((issue) => issue.status === "cancelled").length; return { active, done, cancelled, total: childIssues.length }; } function compactAgentName(run: LedgerRun, agentMap: ReadonlyMap>) { return run.agentName ?? agentMap.get(run.agentId)?.name ?? run.agentId.slice(0, 8); } export function IssueRunLedger({ issueId, issueStatus, childIssues, agentMap, hasLiveRuns, }: IssueRunLedgerProps) { const { data: runs } = useQuery({ queryKey: queryKeys.issues.runs(issueId), queryFn: () => activityApi.runsForIssue(issueId), refetchInterval: hasLiveRuns ? 5000 : false, placeholderData: keepPreviousDataForSameQueryTail(issueId), }); const { data: liveRuns } = useQuery({ queryKey: queryKeys.issues.liveRuns(issueId), queryFn: () => heartbeatsApi.liveRunsForIssue(issueId), enabled: hasLiveRuns, refetchInterval: 3000, placeholderData: keepPreviousDataForSameQueryTail(issueId), }); const { data: activeRun = null } = useQuery({ queryKey: queryKeys.issues.activeRun(issueId), queryFn: () => heartbeatsApi.activeRunForIssue(issueId), enabled: hasLiveRuns || issueStatus === "in_progress", refetchInterval: hasLiveRuns ? false : 3000, placeholderData: keepPreviousDataForSameQueryTail(issueId), }); return ( ); } export function IssueRunLedgerContent({ runs, liveRuns, activeRun, issueStatus, childIssues, agentMap, }: IssueRunLedgerContentProps) { const ledgerRuns = useMemo(() => mergeRuns(runs, liveRuns, activeRun), [activeRun, liveRuns, runs]); const latestRun = ledgerRuns[0] ?? null; const children = childIssueSummary(childIssues); return (

Run ledger

{latestRun ? runSummary(latestRun, agentMap) : issueStatus === "in_progress" ? "Waiting for the first run record." : "No runs linked yet."}

{latestRun ? ( Latest run ) : null}
{children.total > 0 ? (
Child work {children.active.length > 0 ? `${children.active.length} active, ${children.done} done, ${children.cancelled} cancelled` : `all ${children.total} terminal (${children.done} done, ${children.cancelled} cancelled)`}
{children.active.length > 0 ? (
{children.active.slice(0, 4).map((child) => ( {child.identifier ?? child.id.slice(0, 8)} {child.title} {statusLabel(child.status)} ))} {children.active.length > 4 ? ( +{children.active.length - 4} more ) : null}
) : null}
) : null} {ledgerRuns.length === 0 ? (
Historical runs without liveness metadata will appear here once linked to this issue.
) : (
{ledgerRuns.slice(0, 8).map((run) => { const liveness = livenessCopyForRun(run); const stopReason = stopReasonLabel(run); const duration = formatDuration(run.startedAt, run.finishedAt); const exhausted = hasExhaustedContinuation(run); const continuation = continuationLabel(run); return (
{run.runId.slice(0, 8)} {statusLabel(run.status)} {run.isLive ? ( live ) : null} {liveness.label} {exhausted ? ( Exhausted ) : null} {continuation ? ( {continuation} ) : null}
Elapsed{" "} {duration ?? "unknown"}
Last useful action{" "} {lastUsefulActionLabel(run)}
Stop{" "} {stopStatusLabel(run, stopReason)}
{run.livenessReason ? (

{run.livenessReason}

) : null} {run.nextAction ? (
Next action: {run.nextAction}
) : null}
); })} {ledgerRuns.length > 8 ? (
{ledgerRuns.length - 8} older runs not shown
) : null}
)}
); }