diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index bc938910..0213ded2 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -28,6 +28,8 @@ import { assertCompanyAccess, getActorInfo } from "./authz.js"; import { shouldWakeAssigneeOnCheckout } from "./issues-checkout-wakeup.js"; import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; +const MAX_ISSUE_COMMENT_LIMIT = 500; + export function issueRoutes(db: Db, storage: StorageService) { const router = Router(); const svc = issueService(db); @@ -878,7 +880,10 @@ export function issueRoutes(db: Db, storage: StorageService) { typeof req.query.limit === "string" && req.query.limit.trim().length > 0 ? Number(req.query.limit) : null; - const limit = limitRaw && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : null; + const limit = + limitRaw && Number.isFinite(limitRaw) && limitRaw > 0 + ? Math.min(Math.floor(limitRaw), MAX_ISSUE_COMMENT_LIMIT) + : null; const comments = await svc.listComments(id, { afterCommentId, order, diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 1a9dba74..eec83d08 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -623,6 +623,19 @@ export function heartbeatService(db: Db) { .then((rows) => rows[0] ?? null); } + async function getOldestRunForSession(agentId: string, sessionId: string) { + return db + .select({ + id: heartbeatRuns.id, + createdAt: heartbeatRuns.createdAt, + }) + .from(heartbeatRuns) + .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.sessionIdAfter, sessionId))) + .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id)) + .limit(1) + .then((rows) => rows[0] ?? null); + } + async function resolveNormalizedUsageForSession(input: { agentId: string; runId: string; @@ -672,6 +685,7 @@ export function heartbeatService(db: Db) { }; } + const fetchLimit = Math.max(policy.maxSessionRuns > 0 ? policy.maxSessionRuns + 1 : 0, 4); const runs = await db .select({ id: heartbeatRuns.id, @@ -683,7 +697,7 @@ export function heartbeatService(db: Db) { .from(heartbeatRuns) .where(and(eq(heartbeatRuns.agentId, agent.id), eq(heartbeatRuns.sessionIdAfter, sessionId))) .orderBy(desc(heartbeatRuns.createdAt)) - .limit(Math.max(policy.maxSessionRuns + 1, 4)); + .limit(fetchLimit); if (runs.length === 0) { return { @@ -695,7 +709,10 @@ export function heartbeatService(db: Db) { } const latestRun = runs[0] ?? null; - const oldestRun = runs[runs.length - 1] ?? latestRun; + const oldestRun = + policy.maxSessionAgeHours > 0 + ? await getOldestRunForSession(agent.id, sessionId) + : runs[runs.length - 1] ?? latestRun; const latestRawUsage = readRawUsageTotals(latestRun?.usageJson); const sessionAgeHours = latestRun && oldestRun diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index d6f5a643..c42ac02a 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -27,6 +27,7 @@ import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallbac import { getDefaultCompanyGoal } from "./goals.js"; const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"]; +const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500; function assertTransition(from: string, to: string) { if (from === to) return; @@ -1070,7 +1071,10 @@ export function issueService(db: Db) { ) => { const order = opts?.order === "asc" ? "asc" : "desc"; const afterCommentId = opts?.afterCommentId?.trim() || null; - const limit = opts?.limit && opts.limit > 0 ? Math.floor(opts.limit) : null; + const limit = + opts?.limit && opts.limit > 0 + ? Math.min(Math.floor(opts.limit), MAX_ISSUE_COMMENT_PAGE_LIMIT) + : null; const conditions = [eq(issueComments.issueId, issueId)]; if (afterCommentId) { @@ -1085,7 +1089,15 @@ export function issueService(db: Db) { if (!anchor) return []; conditions.push( - sql`(${issueComments.createdAt} > ${anchor.createdAt} OR (${issueComments.createdAt} = ${anchor.createdAt} AND ${issueComments.id} <> ${anchor.id}))`, + order === "asc" + ? sql`( + ${issueComments.createdAt} > ${anchor.createdAt} + OR (${issueComments.createdAt} = ${anchor.createdAt} AND ${issueComments.id} > ${anchor.id}) + )` + : sql`( + ${issueComments.createdAt} < ${anchor.createdAt} + OR (${issueComments.createdAt} = ${anchor.createdAt} AND ${issueComments.id} < ${anchor.id}) + )`, ); } @@ -1093,7 +1105,10 @@ export function issueService(db: Db) { .select() .from(issueComments) .where(and(...conditions)) - .orderBy(order === "asc" ? asc(issueComments.createdAt) : desc(issueComments.createdAt)); + .orderBy( + order === "asc" ? asc(issueComments.createdAt) : desc(issueComments.createdAt), + order === "asc" ? asc(issueComments.id) : desc(issueComments.id), + ); const comments = limit ? await query.limit(limit) : await query; return comments.map(redactIssueComment);