Slim heartbeat run list payloads
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))}`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user