diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 44ac15e6..077332c6 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -1222,6 +1222,72 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(comments[0]?.body).toBe("Comment should be visible"); }); + it("lists user comments when a candidate attribution run log is missing", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Comments issue with missing run log", + status: "todo", + priority: "medium", + }); + + await db.insert(heartbeatRuns).values({ + id: randomUUID(), + companyId, + agentId, + contextSnapshot: { issueId }, + createdAt: new Date("2026-05-12T22:58:00.000Z"), + startedAt: new Date("2026-05-12T22:58:00.000Z"), + finishedAt: new Date("2026-05-12T23:14:00.000Z"), + logStore: "local_file", + logRef: "missing/run-log.ndjson", + logBytes: 128, + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + authorUserId: "user-1", + body: "Comment should still be visible", + createdAt: new Date("2026-05-12T23:00:00.000Z"), + updatedAt: new Date("2026-05-12T23:00:00.000Z"), + }); + + const comments = await svc.listComments(issueId, { + order: "desc", + limit: 50, + }); + + expect(comments.map((comment) => comment.id)).toEqual([commentId]); + expect(comments[0]?.body).toBe("Comment should still be visible"); + expect(comments[0]?.metadata).toBeNull(); + }); + it("includes blockedBy summaries on list rows in one batched pass", async () => { const companyId = randomUUID(); const blockerId = randomUUID(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index e0a3f1a5..fb47582f 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -50,7 +50,8 @@ import { isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@paperclipai/shared"; -import { conflict, notFound, unprocessable } from "../errors.js"; +import { conflict, HttpError, notFound, unprocessable } from "../errors.js"; +import { logger } from "../middleware/logger.js"; import { parseObject } from "../adapters/utils.js"; import { defaultIssueExecutionWorkspaceSettingsForProject, @@ -2804,6 +2805,7 @@ export function issueService(db: Db) { } async function readRunLogText(run: { + runId?: string | null; logStore: string | null; logRef: string | null; logBytes: number | null; @@ -2817,19 +2819,30 @@ export function issueService(db: Db) { let content = ""; let nextOffset: number | undefined = 0; - while (nextOffset !== undefined) { - const remainingBytes = ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES - Buffer.byteLength(content, "utf8"); - if (remainingBytes <= 0) break; - const chunk = await store.read( - { store: "local_file", logRef: run.logRef }, - { - offset, - limitBytes: Math.min(ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES, remainingBytes), - }, - ); - content += chunk.content; - nextOffset = chunk.nextOffset; - offset = chunk.nextOffset ?? 0; + try { + while (nextOffset !== undefined) { + const remainingBytes = ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES - Buffer.byteLength(content, "utf8"); + if (remainingBytes <= 0) break; + const chunk = await store.read( + { store: "local_file", logRef: run.logRef }, + { + offset, + limitBytes: Math.min(ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES, remainingBytes), + }, + ); + content += chunk.content; + nextOffset = chunk.nextOffset; + offset = chunk.nextOffset ?? 0; + } + } catch (err) { + if (err instanceof HttpError && err.status === 404) { + logger.warn( + { err, runId: run.runId ?? undefined, logRef: run.logRef }, + "missing heartbeat run log while deriving issue comment metadata", + ); + return content; + } + throw err; } return content;