Slim heartbeat run list payloads

This commit is contained in:
Dotta
2026-03-10 21:16:33 -05:00
parent 5f76d03913
commit 92aef9bae8
5 changed files with 85 additions and 3 deletions

View File

@@ -1346,6 +1346,17 @@ export function agentRoutes(db: Db) {
res.json(liveRuns);
});
router.get("/heartbeat-runs/:runId", async (req, res) => {
const runId = req.params.runId as string;
const run = await heartbeat.getRun(runId);
if (!run) {
res.status(404).json({ error: "Heartbeat run not found" });
return;
}
assertCompanyAccess(req, run.companyId);
res.json(run);
});
router.post("/heartbeat-runs/:runId/cancel", async (req, res) => {
assertBoard(req);
const runId = req.params.runId as string;

View File

@@ -46,6 +46,69 @@ const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
const startLocksByAgent = new Map<string, Promise<void>>();
const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
const summarizedHeartbeatRunResultJson = sql<Record<string, unknown> | null>`
CASE
WHEN ${heartbeatRuns.resultJson} IS NULL THEN NULL
ELSE NULLIF(
jsonb_strip_nulls(
jsonb_build_object(
'summary', CASE
WHEN ${heartbeatRuns.resultJson} ->> 'summary' IS NULL THEN NULL
ELSE left(${heartbeatRuns.resultJson} ->> 'summary', 500)
END,
'result', CASE
WHEN ${heartbeatRuns.resultJson} ->> 'result' IS NULL THEN NULL
ELSE left(${heartbeatRuns.resultJson} ->> 'result', 500)
END,
'message', CASE
WHEN ${heartbeatRuns.resultJson} ->> 'message' IS NULL THEN NULL
ELSE left(${heartbeatRuns.resultJson} ->> 'message', 500)
END,
'error', CASE
WHEN ${heartbeatRuns.resultJson} ->> 'error' IS NULL THEN NULL
ELSE left(${heartbeatRuns.resultJson} ->> 'error', 500)
END,
'total_cost_usd', ${heartbeatRuns.resultJson} -> 'total_cost_usd',
'cost_usd', ${heartbeatRuns.resultJson} -> 'cost_usd',
'costUsd', ${heartbeatRuns.resultJson} -> 'costUsd'
)
),
'{}'::jsonb
)
END
`;
const heartbeatRunListColumns = {
id: heartbeatRuns.id,
companyId: heartbeatRuns.companyId,
agentId: heartbeatRuns.agentId,
invocationSource: heartbeatRuns.invocationSource,
triggerDetail: heartbeatRuns.triggerDetail,
status: heartbeatRuns.status,
startedAt: heartbeatRuns.startedAt,
finishedAt: heartbeatRuns.finishedAt,
error: heartbeatRuns.error,
wakeupRequestId: heartbeatRuns.wakeupRequestId,
exitCode: heartbeatRuns.exitCode,
signal: heartbeatRuns.signal,
usageJson: heartbeatRuns.usageJson,
resultJson: summarizedHeartbeatRunResultJson.as("resultJson"),
sessionIdBefore: heartbeatRuns.sessionIdBefore,
sessionIdAfter: heartbeatRuns.sessionIdAfter,
logStore: heartbeatRuns.logStore,
logRef: heartbeatRuns.logRef,
logBytes: heartbeatRuns.logBytes,
logSha256: heartbeatRuns.logSha256,
logCompressed: heartbeatRuns.logCompressed,
stdoutExcerpt: sql<string | null>`NULL`.as("stdoutExcerpt"),
stderrExcerpt: sql<string | null>`NULL`.as("stderrExcerpt"),
errorCode: heartbeatRuns.errorCode,
externalRunId: heartbeatRuns.externalRunId,
contextSnapshot: heartbeatRuns.contextSnapshot,
createdAt: heartbeatRuns.createdAt,
updatedAt: heartbeatRuns.updatedAt,
} as const;
function appendExcerpt(prev: string, chunk: string) {
return appendWithCap(prev, chunk, MAX_EXCERPT_BYTES);
}
@@ -2260,9 +2323,9 @@ export function heartbeatService(db: Db) {
}
return {
list: (companyId: string, agentId?: string, limit?: number) => {
list: async (companyId: string, agentId?: string, limit?: number) => {
const query = db
.select()
.select(heartbeatRunListColumns)
.from(heartbeatRuns)
.where(
agentId

View File

@@ -29,6 +29,7 @@ export const heartbeatsApi = {
const qs = searchParams.toString();
return api.get<HeartbeatRun[]>(`/companies/${companyId}/heartbeat-runs${qs ? `?${qs}` : ""}`);
},
get: (runId: string) => api.get<HeartbeatRun>(`/heartbeat-runs/${runId}`),
events: (runId: string, afterSeq = 0, limit = 200) =>
api.get<HeartbeatRunEvent[]>(
`/heartbeat-runs/${runId}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=${encodeURIComponent(String(limit))}`,

View File

@@ -68,6 +68,7 @@ export const queryKeys = {
["costs", companyId, from, to] as const,
heartbeats: (companyId: string, agentId?: string) =>
["heartbeats", companyId, agentId] as const,
runDetail: (runId: string) => ["heartbeat-run", runId] as const,
liveRuns: (companyId: string) => ["live-runs", companyId] as const,
runIssues: (runId: string) => ["run-issues", runId] as const,
org: (companyId: string) => ["org", companyId] as const,

View File

@@ -1254,9 +1254,15 @@ function RunsTab({
/* ---- Run Detail (expanded) ---- */
function RunDetail({ run, agentRouteId, adapterType }: { run: HeartbeatRun; agentRouteId: string; adapterType: string }) {
function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: HeartbeatRun; agentRouteId: string; adapterType: string }) {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { data: hydratedRun } = useQuery({
queryKey: queryKeys.runDetail(initialRun.id),
queryFn: () => heartbeatsApi.get(initialRun.id),
enabled: Boolean(initialRun.id),
});
const run = hydratedRun ?? initialRun;
const metrics = runMetrics(run);
const [sessionOpen, setSessionOpen] = useState(false);
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);