[codex] Add runtime lifecycle recovery and live issue visibility (#4419)

This commit is contained in:
Dotta 2026-04-24 15:50:32 -05:00 committed by GitHub
parent 9a8d219949
commit 5a0c1979cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 9625 additions and 2044 deletions

View file

@ -0,0 +1,30 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockApi = vi.hoisted(() => ({
get: vi.fn(),
}));
vi.mock("./client", () => ({
api: mockApi,
}));
import { heartbeatsApi } from "./heartbeats";
describe("heartbeatsApi.liveRunsForCompany", () => {
beforeEach(() => {
mockApi.get.mockReset();
mockApi.get.mockResolvedValue([]);
});
it("keeps the legacy numeric minCount signature", async () => {
await heartbeatsApi.liveRunsForCompany("company-1", 4);
expect(mockApi.get).toHaveBeenCalledWith("/companies/company-1/live-runs?minCount=4");
});
it("passes minCount and limit options to the company live-runs endpoint", async () => {
await heartbeatsApi.liveRunsForCompany("company-1", { minCount: 50, limit: 50 });
expect(mockApi.get).toHaveBeenCalledWith("/companies/company-1/live-runs?minCount=50&limit=50");
});
});

View file

@ -1,4 +1,9 @@
import type { HeartbeatRun, HeartbeatRunEvent, InstanceSchedulerHeartbeatAgent, WorkspaceOperation } from "@paperclipai/shared";
import type {
HeartbeatRun,
HeartbeatRunEvent,
InstanceSchedulerHeartbeatAgent,
WorkspaceOperation,
} from "@paperclipai/shared";
import { api } from "./client";
export interface RunLivenessFields {
@ -20,12 +25,15 @@ export interface ActiveRunForIssue {
agentId: string;
agentName: string;
adapterType: string;
logBytes?: number | null;
lastOutputBytes?: number | null;
issueId?: string | null;
livenessState?: RunLivenessFields["livenessState"];
livenessReason?: string | null;
continuationAttempt?: number;
lastUsefulActionAt?: string | Date | null;
nextAction?: string | null;
outputSilence?: HeartbeatRun["outputSilence"];
}
export interface LiveRunForIssue {
@ -39,12 +47,23 @@ export interface LiveRunForIssue {
agentId: string;
agentName: string;
adapterType: string;
logBytes?: number | null;
lastOutputBytes?: number | null;
issueId?: string | null;
livenessState?: RunLivenessFields["livenessState"];
livenessReason?: string | null;
continuationAttempt?: number;
lastUsefulActionAt?: string | null;
nextAction?: string | null;
outputSilence?: HeartbeatRun["outputSilence"];
}
export interface WatchdogDecisionInput {
runId: string;
decision: "snooze" | "continue" | "dismissed_false_positive";
evaluationIssueId?: string | null;
reason?: string | null;
snoozedUntil?: string | null;
}
export const heartbeatsApi = {
@ -71,12 +90,31 @@ export const heartbeatsApi = {
`/workspace-operations/${operationId}/log?offset=${encodeURIComponent(String(offset))}&limitBytes=${encodeURIComponent(String(limitBytes))}`,
),
cancel: (runId: string) => api.post<void>(`/heartbeat-runs/${runId}/cancel`, {}),
recordWatchdogDecision: (input: WatchdogDecisionInput) =>
api.post(`/heartbeat-runs/${input.runId}/watchdog-decisions`, {
decision: input.decision,
evaluationIssueId: input.evaluationIssueId ?? null,
reason: input.reason ?? null,
snoozedUntil: input.snoozedUntil ?? null,
}),
liveRunsForIssue: (issueId: string) =>
api.get<LiveRunForIssue[]>(`/issues/${issueId}/live-runs`),
activeRunForIssue: (issueId: string) =>
api.get<ActiveRunForIssue | null>(`/issues/${issueId}/active-run`),
liveRunsForCompany: (companyId: string, minCount?: number) =>
api.get<LiveRunForIssue[]>(`/companies/${companyId}/live-runs${minCount ? `?minCount=${minCount}` : ""}`),
liveRunsForCompany: (
companyId: string,
options?: number | { minCount?: number; limit?: number },
) => {
const searchParams = new URLSearchParams();
if (typeof options === "number") {
searchParams.set("minCount", String(options));
} else if (options) {
if (options.minCount) searchParams.set("minCount", String(options.minCount));
if (options.limit) searchParams.set("limit", String(options.limit));
}
const qs = searchParams.toString();
return api.get<LiveRunForIssue[]>(`/companies/${companyId}/live-runs${qs ? `?${qs}` : ""}`);
},
listInstanceSchedulerAgents: () =>
api.get<InstanceSchedulerHeartbeatAgent[]>("/instance/scheduler-heartbeats"),
};