mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 19:00:38 +09:00
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Reliable execution depends on heartbeat routing, issue lifecycle semantics, telemetry, and a fast enough local verification loop to keep regressions visible > - The remaining commits on this branch were mostly server/runtime correctness fixes plus test and documentation follow-ups in that area > - Those changes are logically separate from the UI-focused issue-detail and workspace/navigation branches even when they touch overlapping issue APIs > - This pull request groups the execution reliability, heartbeat, telemetry, and tooling changes into one standalone branch > - The benefit is a focused review of the control-plane correctness work, including the follow-up fix that restored the implicit comment-reopen helpers after branch splitting ## What Changed - Hardened issue/heartbeat execution behavior, including self-review stage skipping, deferred mention wakes during active execution, stranded execution recovery, active-run scoping, assignee resolution, and blocked-to-todo wake resumption - Reduced noisy polling/logging overhead by trimming issue run payloads, compacting persisted run logs, silencing high-volume request logs, and capping heartbeat-run queries in dashboard/inbox surfaces - Expanded telemetry and status semantics with adapter/model fields on task completion plus clearer status guidance in docs/onboarding material - Updated test infrastructure and verification defaults with faster route-test module isolation, cheaper default `pnpm test`, e2e isolation from local state, and repo verification follow-ups - Included docs/release housekeeping from the branch and added a small follow-up commit restoring the implicit comment-reopen helpers that were dropped during branch reconstruction ## Verification - `pnpm vitest run server/src/__tests__/issue-comment-reopen-routes.test.ts server/src/__tests__/issue-telemetry-routes.test.ts` - `pnpm vitest run server/src/__tests__/http-log-policy.test.ts server/src/__tests__/heartbeat-run-log.test.ts server/src/__tests__/health.test.ts` - `server/src/__tests__/activity-service.test.ts`, `server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and `server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted on this host but the embedded Postgres harness reported init-script/data-dir problems and skipped or failed to start, so they are noted as environment-limited ## Risks - Medium: this branch changes core issue/heartbeat routing and reopen/wakeup behavior, so regressions would affect agent execution flow rather than isolated UI polish - Because it also updates verification infrastructure, reviewers should pay attention to whether the new tests are asserting the right failure modes and not just reshaping harness behavior ## Model Used - OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact deployed model ID is not exposed in this environment), reasoning enabled, tool use and local code execution enabled ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
e89076148a
commit
7f893ac4ec
106 changed files with 4682 additions and 713 deletions
|
|
@ -27,7 +27,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import { useToastActions } from "@/context/ToastContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
||||
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
||||
|
|
@ -255,7 +255,7 @@ export function AdapterManager() {
|
|||
const { selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
const [installPackage, setInstallPackage] = useState("");
|
||||
const [installVersion, setInstallVersion] = useState("");
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { issuesApi } from "../api/issues";
|
|||
import { usePanel } from "../context/PanelContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
|
@ -1540,7 +1540,7 @@ function ConfigurationTab({
|
|||
hideInstructionsFile?: boolean;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false);
|
||||
const lastAgentRef = useRef(agent);
|
||||
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ export function Agents() {
|
|||
});
|
||||
|
||||
const { data: runs } = useQuery({
|
||||
queryKey: queryKeys.heartbeats(selectedCompanyId!),
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!),
|
||||
queryKey: [...queryKeys.liveRuns(selectedCompanyId!), "agents-page"],
|
||||
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
import { useNavigate, useLocation } from "@/lib/router";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { authApi } from "../api/auth";
|
||||
import { companiesApi } from "../api/companies";
|
||||
|
|
@ -580,7 +580,7 @@ function expandAncestors(filePath: string): string[] {
|
|||
export function CompanyExport() {
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { data: session, isFetched: isSessionFetched } = useQuery({
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from "@paperclipai/shared";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { authApi } from "../api/auth";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { agentsApi } from "../api/agents";
|
||||
|
|
@ -651,7 +651,7 @@ export function CompanyImport() {
|
|||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
const packageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { data: session } = useQuery({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||
import { DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION } from "@paperclipai/shared";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { accessApi } from "../api/access";
|
||||
import { assetsApi } from "../api/assets";
|
||||
|
|
@ -34,7 +34,7 @@ export function CompanySettings() {
|
|||
setSelectedCompanyId
|
||||
} = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
// General settings local state
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type {
|
|||
import { companySkillsApi } from "../api/companySkills";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { MarkdownBody } from "../components/MarkdownBody";
|
||||
|
|
@ -530,7 +530,7 @@ function SkillPane({
|
|||
onSave: () => void;
|
||||
savePending: boolean;
|
||||
}) {
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
if (!detail) {
|
||||
if (loading) {
|
||||
|
|
@ -759,7 +759,7 @@ export function CompanySkills() {
|
|||
const queryClient = useQueryClient();
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const [skillFilter, setSkillFilter] = useState("");
|
||||
const [source, setSource] = useState("");
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
|||
import type { Agent, Issue } from "@paperclipai/shared";
|
||||
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||
|
||||
const DASHBOARD_HEARTBEAT_RUN_LIMIT = 100;
|
||||
|
||||
function getRecentIssues(issues: Issue[]): Issue[] {
|
||||
return [...issues]
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
|
|
@ -75,8 +77,8 @@ export function Dashboard() {
|
|||
});
|
||||
|
||||
const { data: runs } = useQuery({
|
||||
queryKey: queryKeys.heartbeats(selectedCompanyId!),
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!),
|
||||
queryKey: [...queryKeys.heartbeats(selectedCompanyId!), "limit", DASHBOARD_HEARTBEAT_RUN_LIMIT],
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!, undefined, DASHBOARD_HEARTBEAT_RUN_LIMIT),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ import {
|
|||
Search,
|
||||
ListTree,
|
||||
} from "lucide-react";
|
||||
|
||||
const INBOX_HEARTBEAT_RUN_LIMIT = 200;
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||
|
|
@ -799,8 +801,8 @@ export function Inbox() {
|
|||
});
|
||||
|
||||
const { data: heartbeatRuns, isLoading: isRunsLoading } = useQuery({
|
||||
queryKey: queryKeys.heartbeats(selectedCompanyId!),
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!),
|
||||
queryKey: [...queryKeys.heartbeats(selectedCompanyId!), "limit", INBOX_HEARTBEAT_RUN_LIMIT],
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!, undefined, INBOX_HEARTBEAT_RUN_LIMIT),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import { projectsApi } from "../api/projects";
|
|||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees";
|
||||
import { extractIssueTimelineEvents } from "../lib/issue-timeline-events";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
|
@ -853,7 +853,7 @@ export function IssueDetail() {
|
|||
const navigate = useNavigate();
|
||||
const navigationType = useNavigationType();
|
||||
const location = useLocation();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const { isMobile } = useSidebar();
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import { useToastActions } from "@/context/ToastContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
||||
|
|
@ -64,7 +64,7 @@ export function PluginManager() {
|
|||
const { selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
|
||||
const [installPackage, setInstallPackage] = useState("");
|
||||
const [installDialogOpen, setInstallDialogOpen] = useState(false);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { heartbeatsApi } from "../api/heartbeats";
|
|||
import { assetsApi } from "../api/assets";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { ProjectProperties, type ProjectConfigFieldKey, type ProjectFieldSaveState } from "../components/ProjectProperties";
|
||||
|
|
@ -330,7 +330,7 @@ export function ProjectDetail() {
|
|||
const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany();
|
||||
const { closePanel } = usePanel();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { agentsApi } from "../api/agents";
|
|||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { buildRoutineTriggerPatch } from "../lib/routine-trigger-patch";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
|
|
@ -268,7 +268,7 @@ export function RoutineDetail() {
|
|||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const hydratedRoutineIdRef = useRef<string | null>(null);
|
||||
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ vi.mock("../context/BreadcrumbContext", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("../context/ToastContext", () => ({
|
||||
useToast: () => ({ pushToast: vi.fn() }),
|
||||
useToastActions: () => ({ pushToast: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("../api/routines", () => ({
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { issuesApi } from "../api/issues";
|
|||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useToastActions } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { groupBy } from "../lib/groupBy";
|
||||
import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
||||
|
|
@ -293,7 +293,7 @@ export function Routines() {
|
|||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { pushToast } = useToast();
|
||||
const { pushToast } = useToastActions();
|
||||
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
||||
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const assigneeSelectorRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue