[PAP-3154] Stop padding /live-runs by default (#4963)

## Summary
- Fix [PAP-3154](/PAP/issues/PAP-3154): the Sidebar's "Dashboard NN
live" badge showed a constant 50 in every company because `GET
/api/companies/:companyId/live-runs` was padding its response with up to
50 recent (non-live) heartbeat runs whenever the caller did not pass
`minCount`.
- Regression introduced by
[#4875](https://github.com/paperclipai/paperclip/pull/4875) (commit
`6445bef9`), which capped both `minCount` and `limit` at 50 with a
fallback of 50 for omitted values. The cap is correct for `limit` (real
unboundedness guard); for `minCount` it conflates "no padding" with "pad
to the cap".
- Default `minCount` to 0 so callers asking for "live runs" only get
actually-live runs unless they explicitly request padding
(`ActiveAgentsPanel` is the only caller that does). Keep `limit` capped
at 50 by default.

## Test plan
- [x] `pnpm exec vitest run
server/src/__tests__/agent-live-run-routes.test.ts` — 7/7 pass,
including new tests for the no-pad default and explicit padding.
- [x] `pnpm exec vitest run ui/src/components/Sidebar.test.tsx
ui/src/components/ActiveAgentsPanel.test.tsx
ui/src/api/heartbeats.test.ts` — 6/6 pass.
- [ ] Verify in dev: with ~8 truly-live runs in a company, the sidebar
Dashboard badge shows the real count (not 50).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-05-01 10:33:13 -05:00
committed by GitHub
parent 42a299fb9d
commit e273d621fc
2 changed files with 146 additions and 2 deletions
@@ -344,7 +344,7 @@ describe("agent live run routes", () => {
expect(mockHeartbeatService.buildRunOutputSilence).toHaveBeenCalledTimes(50);
});
it("treats explicit zero live run limits as the capped default", async () => {
it("treats explicit zero or invalid live run limit as the capped default", async () => {
const rows = Array.from({ length: 75 }, (_, index) => ({
id: `run-${index}`,
companyId: "company-1",
@@ -381,4 +381,143 @@ describe("agent live run routes", () => {
expect(limit).toHaveBeenCalledWith(50);
expect(res.body).toHaveLength(50);
});
it("does not pad with recent runs when no minCount is requested", async () => {
const liveRows = Array.from({ length: 8 }, (_, index) => ({
id: `run-live-${index}`,
companyId: "company-1",
status: "running",
invocationSource: "on_demand",
triggerDetail: "manual",
startedAt: new Date("2026-04-10T09:30:00.000Z"),
finishedAt: null,
createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`),
agentId: "agent-1",
agentName: "Builder",
adapterType: "codex_local",
logBytes: 0,
livenessState: "healthy",
livenessReason: null,
continuationAttempt: 0,
lastUsefulActionAt: null,
nextAction: null,
lastOutputAt: null,
lastOutputSeq: null,
lastOutputStream: null,
lastOutputBytes: 0,
processStartedAt: null,
issueId: "issue-1",
}));
const selectCalls: Array<ReturnType<typeof vi.fn>> = [];
const db = {
select: vi.fn().mockImplementation(() => {
const limitFn = vi.fn(async (value: number) => liveRows.slice(0, value));
const orderedQuery = {
limit: limitFn,
then: (resolve: (value: typeof liveRows) => unknown) =>
Promise.resolve(liveRows).then(resolve),
};
const query = {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnValue(orderedQuery),
};
selectCalls.push(limitFn);
return query;
}),
};
const res = await requestApp(
await createApp(db),
(baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"),
);
expect(res.status, JSON.stringify(res.body)).toBe(200);
expect(res.body).toHaveLength(8);
expect(db.select).toHaveBeenCalledTimes(1);
});
it("pads with recent runs when minCount is explicitly requested", async () => {
const liveRows = Array.from({ length: 2 }, (_, index) => ({
id: `run-live-${index}`,
companyId: "company-1",
status: "running",
invocationSource: "on_demand",
triggerDetail: "manual",
startedAt: new Date("2026-04-10T09:30:00.000Z"),
finishedAt: null,
createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`),
agentId: "agent-1",
agentName: "Builder",
adapterType: "codex_local",
logBytes: 0,
livenessState: "healthy",
livenessReason: null,
continuationAttempt: 0,
lastUsefulActionAt: null,
nextAction: null,
lastOutputAt: null,
lastOutputSeq: null,
lastOutputStream: null,
lastOutputBytes: 0,
processStartedAt: null,
issueId: "issue-1",
}));
const recentRows = Array.from({ length: 4 }, (_, index) => ({
id: `run-recent-${index}`,
companyId: "company-1",
status: "succeeded",
invocationSource: "on_demand",
triggerDetail: "manual",
startedAt: new Date("2026-04-09T09:30:00.000Z"),
finishedAt: new Date("2026-04-09T09:35:00.000Z"),
createdAt: new Date(`2026-04-09T09:${String(index % 60).padStart(2, "0")}:00.000Z`),
agentId: "agent-1",
agentName: "Builder",
adapterType: "codex_local",
logBytes: 0,
livenessState: "healthy",
livenessReason: null,
continuationAttempt: 0,
lastUsefulActionAt: null,
nextAction: null,
lastOutputAt: null,
lastOutputSeq: null,
lastOutputStream: null,
lastOutputBytes: 0,
processStartedAt: null,
issueId: "issue-1",
}));
let selectCallCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCallCount += 1;
const rows = selectCallCount === 1 ? liveRows : recentRows;
const limitFn = vi.fn(async (value: number) => rows.slice(0, value));
const orderedQuery = {
limit: limitFn,
then: (resolve: (value: typeof rows) => unknown) =>
Promise.resolve(rows).then(resolve),
};
return {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnValue(orderedQuery),
};
}),
};
const res = await requestApp(
await createApp(db),
(baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?minCount=4"),
);
expect(res.status, JSON.stringify(res.body)).toBe(200);
expect(res.body).toHaveLength(4);
expect(db.select).toHaveBeenCalledTimes(2);
});
});
+6 -1
View File
@@ -2844,7 +2844,12 @@ export function agentRoutes(
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
const minCount = readLiveRunsQueryInt(req.query.minCount, 50, 50);
// `minCount` is a padding floor for callers that want a minimum number of
// recent runs to render (e.g. dashboard cards). It must default to 0 so
// callers asking for "live runs" get only actually-live runs — otherwise
// every caller with no minCount param gets up to 50 historical runs
// padded in and renders bogus "live" counts.
const minCount = readLiveRunsQueryInt(req.query.minCount, 50, 0);
const limit = readLiveRunsQueryInt(req.query.limit, 50, 50);
const columns = {