From 10ade0582bcee45d805dfc27eacd3318ac4e99bf Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 15 Apr 2026 08:24:26 -0500 Subject: [PATCH] Narrow heartbeat log endpoint lookups (cherry picked from commit 46b224081b2a5aa286da53f8ec0f530d7ccccc90) --- .../__tests__/agent-live-run-routes.test.ts | 38 +++++++++++++++++++ server/src/routes/agents.ts | 4 +- server/src/services/heartbeat.ts | 35 +++++++++++++++-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index 061d640d887..e471c3728ec 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -9,6 +9,8 @@ const mockAgentService = vi.hoisted(() => ({ const mockHeartbeatService = vi.hoisted(() => ({ getRunIssueSummary: vi.fn(), getActiveRunIssueSummaryForAgent: vi.fn(), + getRunLogAccess: vi.fn(), + readLog: vi.fn(), })); const mockIssueService = vi.hoisted(() => ({ @@ -100,6 +102,19 @@ describe("agent live run routes", () => { issueId: "issue-1", }); mockHeartbeatService.getActiveRunIssueSummaryForAgent.mockResolvedValue(null); + mockHeartbeatService.getRunLogAccess.mockResolvedValue({ + id: "run-1", + companyId: "company-1", + logStore: "local_file", + logRef: "logs/run-1.ndjson", + }); + mockHeartbeatService.readLog.mockResolvedValue({ + runId: "run-1", + store: "local_file", + logRef: "logs/run-1.ndjson", + content: "chunk", + nextOffset: 5, + }); }); it("returns a compact active run payload for issue polling", async () => { @@ -163,4 +178,27 @@ describe("agent live run routes", () => { adapterType: "codex_local", }); }); + + it("uses narrow run log metadata lookups for log polling", async () => { + const res = await request(await createApp()).get("/api/heartbeat-runs/run-1/log?offset=12&limitBytes=64"); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockHeartbeatService.getRunLogAccess).toHaveBeenCalledWith("run-1"); + expect(mockHeartbeatService.readLog).toHaveBeenCalledWith({ + id: "run-1", + companyId: "company-1", + logStore: "local_file", + logRef: "logs/run-1.ndjson", + }, { + offset: 12, + limitBytes: 64, + }); + expect(res.body).toEqual({ + runId: "run-1", + store: "local_file", + logRef: "logs/run-1.ndjson", + content: "chunk", + nextOffset: 5, + }); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 9511d6d0daa..fb72a6d7ba6 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2479,7 +2479,7 @@ export function agentRoutes(db: Db) { router.get("/heartbeat-runs/:runId/log", async (req, res) => { const runId = req.params.runId as string; - const run = await heartbeat.getRun(runId); + const run = await heartbeat.getRunLogAccess(runId); if (!run) { res.status(404).json({ error: "Heartbeat run not found" }); return; @@ -2488,7 +2488,7 @@ export function agentRoutes(db: Db) { const offset = Number(req.query.offset ?? 0); const limitBytes = Number(req.query.limitBytes ?? 256000); - const result = await heartbeat.readLog(runId, { + const result = await heartbeat.readLog(run, { offset: Number.isFinite(offset) ? offset : 0, limitBytes: Number.isFinite(limitBytes) ? limitBytes : 256000, }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index f77858ab1c5..fbb25340edf 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -473,6 +473,13 @@ const heartbeatRunSafeColumns = { resultJson: heartbeatRunSafeResultJsonColumn, } as const; +const heartbeatRunLogAccessColumns = { + id: heartbeatRuns.id, + companyId: heartbeatRuns.companyId, + logStore: heartbeatRuns.logStore, + logRef: heartbeatRuns.logRef, +} as const; + const heartbeatRunIssueSummaryColumns = { id: heartbeatRuns.id, status: heartbeatRuns.status, @@ -1513,6 +1520,14 @@ export function heartbeatService(db: Db) { .then((rows) => rows[0] ?? null); } + async function getRunLogAccess(runId: string) { + return db + .select(heartbeatRunLogAccessColumns) + .from(heartbeatRuns) + .where(eq(heartbeatRuns.id, runId)) + .then((rows) => rows[0] ?? null); + } + async function getIssueExecutionContext(companyId: string, issueId: string) { return db .select({ @@ -2942,7 +2957,10 @@ export function heartbeatService(db: Db) { async function escalateStrandedAssignedIssue(input: { issue: typeof issues.$inferSelect; previousStatus: "todo" | "in_progress"; - latestRun: typeof heartbeatRuns.$inferSelect | null; + latestRun: Pick< + typeof heartbeatRuns.$inferSelect, + "id" | "status" | "error" | "errorCode" | "contextSnapshot" + > | null; comment: string; }) { const updated = await issuesSvc.update(input.issue.id, { @@ -5175,6 +5193,8 @@ export function heartbeatService(db: Db) { getRun, + getRunLogAccess, + getRuntimeState: async (agentId: string) => { const state = await getRuntimeState(agentId); const agent = await getAgent(agentId); @@ -5248,8 +5268,17 @@ export function heartbeatService(db: Db) { .orderBy(asc(heartbeatRunEvents.seq)) .limit(Math.max(1, Math.min(limit, 1000))), - readLog: async (runId: string, opts?: { offset?: number; limitBytes?: number }) => { - const run = await getRun(runId); + readLog: async ( + runOrLookup: string | { + id: string; + companyId: string; + logStore: string | null; + logRef: string | null; + }, + opts?: { offset?: number; limitBytes?: number }, + ) => { + const run = typeof runOrLookup === "string" ? await getRunLogAccess(runOrLookup) : runOrLookup; + const runId = typeof runOrLookup === "string" ? runOrLookup : runOrLookup.id; if (!run) throw notFound("Heartbeat run not found"); if (!run.logStore || !run.logRef) throw notFound("Run log not found");