Slim heartbeat run list payloads
This commit is contained in:
@@ -1346,6 +1346,17 @@ export function agentRoutes(db: Db) {
|
|||||||
res.json(liveRuns);
|
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) => {
|
router.post("/heartbeat-runs/:runId/cancel", async (req, res) => {
|
||||||
assertBoard(req);
|
assertBoard(req);
|
||||||
const runId = req.params.runId as string;
|
const runId = req.params.runId as string;
|
||||||
|
|||||||
@@ -46,6 +46,69 @@ const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
|
|||||||
const startLocksByAgent = new Map<string, Promise<void>>();
|
const startLocksByAgent = new Map<string, Promise<void>>();
|
||||||
const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
|
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) {
|
function appendExcerpt(prev: string, chunk: string) {
|
||||||
return appendWithCap(prev, chunk, MAX_EXCERPT_BYTES);
|
return appendWithCap(prev, chunk, MAX_EXCERPT_BYTES);
|
||||||
}
|
}
|
||||||
@@ -2260,9 +2323,9 @@ export function heartbeatService(db: Db) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list: (companyId: string, agentId?: string, limit?: number) => {
|
list: async (companyId: string, agentId?: string, limit?: number) => {
|
||||||
const query = db
|
const query = db
|
||||||
.select()
|
.select(heartbeatRunListColumns)
|
||||||
.from(heartbeatRuns)
|
.from(heartbeatRuns)
|
||||||
.where(
|
.where(
|
||||||
agentId
|
agentId
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const heartbeatsApi = {
|
|||||||
const qs = searchParams.toString();
|
const qs = searchParams.toString();
|
||||||
return api.get<HeartbeatRun[]>(`/companies/${companyId}/heartbeat-runs${qs ? `?${qs}` : ""}`);
|
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) =>
|
events: (runId: string, afterSeq = 0, limit = 200) =>
|
||||||
api.get<HeartbeatRunEvent[]>(
|
api.get<HeartbeatRunEvent[]>(
|
||||||
`/heartbeat-runs/${runId}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=${encodeURIComponent(String(limit))}`,
|
`/heartbeat-runs/${runId}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=${encodeURIComponent(String(limit))}`,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const queryKeys = {
|
|||||||
["costs", companyId, from, to] as const,
|
["costs", companyId, from, to] as const,
|
||||||
heartbeats: (companyId: string, agentId?: string) =>
|
heartbeats: (companyId: string, agentId?: string) =>
|
||||||
["heartbeats", companyId, agentId] as const,
|
["heartbeats", companyId, agentId] as const,
|
||||||
|
runDetail: (runId: string) => ["heartbeat-run", runId] as const,
|
||||||
liveRuns: (companyId: string) => ["live-runs", companyId] as const,
|
liveRuns: (companyId: string) => ["live-runs", companyId] as const,
|
||||||
runIssues: (runId: string) => ["run-issues", runId] as const,
|
runIssues: (runId: string) => ["run-issues", runId] as const,
|
||||||
org: (companyId: string) => ["org", companyId] as const,
|
org: (companyId: string) => ["org", companyId] as const,
|
||||||
|
|||||||
@@ -1254,9 +1254,15 @@ function RunsTab({
|
|||||||
|
|
||||||
/* ---- Run Detail (expanded) ---- */
|
/* ---- 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 queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
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 metrics = runMetrics(run);
|
||||||
const [sessionOpen, setSessionOpen] = useState(false);
|
const [sessionOpen, setSessionOpen] = useState(false);
|
||||||
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);
|
const [claudeLoginResult, setClaudeLoginResult] = useState<ClaudeLoginResult | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user