From 14fd8aee36c5cb615570b3c9bb86794c42408aa7 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2026 23:02:01 +0200 Subject: [PATCH] fix(server): flag truncated issue descriptions (#4771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The issue list API is one of the surfaces API consumers use to synchronize issue metadata. > - The list endpoint intentionally returns a bounded `description` preview so large descriptions do not bloat list responses. > - Before this change, that preview looked like a complete field value because the response did not say whether it had been shortened. > - That made round-trip clients vulnerable to accidentally PATCHing a preview back over the full description. > - This pull request keeps the existing preview behavior but adds an explicit `descriptionTruncated` flag. > - The benefit is backwards-compatible visibility into truncated issue descriptions, so clients can avoid data-loss workflows. ## Linked Issues or Issue Description Fixes #4758. Related PR: #4792 also targets #4758, but it includes unrelated logger changes and currently has separate review/security concerns. This PR keeps the fix scoped to the issue-list description truncation API behavior. ## What Changed - Added `descriptionTruncated` to the issue list projection when `description` exceeds the existing 1200-character preview limit. - Exposed `descriptionTruncated?: boolean` on the shared `Issue` type. - Added service tests for truncated descriptions, exact-limit descriptions, null descriptions, and multibyte-safe preview truncation. ## Verification June 18, 2026 refresh after rebasing onto current `origin/master`: - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts` - `pnpm --filter @paperclipai/server typecheck` - `pnpm typecheck` - `git diff --check origin/master...HEAD` - GitHub PR checks are green on head `12e828e6`. Earlier pre-review verification also included `pnpm test`. ## Risks - Low risk. This is an additive API response field; existing clients can ignore it. - The list endpoint still returns the same bounded `description` preview. Clients that need full text should continue fetching the issue detail, but can now detect when that is necessary. - No database migration or UI behavior change. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex based on GPT-5, via Codex desktop on April 29, June 15, and June 18, 2026. Used tool-assisted repository inspection, code editing, local test execution, GitHub CLI workflows, and PR review follow-up. Exact context window size is not surfaced by the tool. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots (N/A: no UI change) - [x] I have updated relevant documentation to reflect my changes (N/A: additive API field covered by tests) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Sami Rusani --- packages/shared/src/types/issue.ts | 1 + server/src/__tests__/issues-service.test.ts | 55 +++++++++++++++++++++ server/src/services/issues.ts | 6 +++ 3 files changed, 62 insertions(+) diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index 0dc0bc030a..da94fbdd15 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -793,6 +793,7 @@ export interface Issue { ancestors?: IssueAncestor[]; title: string; description: string | null; + descriptionTruncated?: boolean; status: IssueStatus; workMode: IssueWorkMode; priority: IssuePriority; diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 825e0048fd..ee0632e673 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -2590,11 +2590,65 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(result).toBeTruthy(); expect(result?.description).toHaveLength(1200); + expect(result?.descriptionTruncated).toBe(true); expect(result?.executionPolicy).toBeNull(); expect(result?.executionState).toBeNull(); expect(result?.executionWorkspaceSettings).toBeNull(); }); + it("marks list descriptions as not truncated when they fit the preview limit", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + const description = "x".repeat(1200); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Exact preview issue", + description, + status: "todo", + priority: "medium", + }); + + const [result] = await svc.list(companyId); + + expect(result?.description).toHaveLength(1200); + expect(result?.descriptionTruncated).toBe(false); + }); + + it("marks null list descriptions as not truncated", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Null description issue", + description: null, + status: "todo", + priority: "medium", + }); + + const [result] = await svc.list(companyId); + + expect(result?.description).toBeNull(); + expect(result?.descriptionTruncated).toBe(false); + }); + it("does not let description preview truncation split multibyte characters", async () => { const companyId = randomUUID(); const issueId = randomUUID(); @@ -2620,6 +2674,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(result?.description).toHaveLength(1200); expect(result?.description?.endsWith("—")).toBe(true); + expect(result?.descriptionTruncated).toBe(true); }); }); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index e7500973b0..93a57137dc 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -3145,6 +3145,12 @@ const issueListSelect = { ) END `, + descriptionTruncated: sql` + CASE + WHEN ${issues.description} IS NULL THEN false + ELSE length(${issues.description}) > ${ISSUE_LIST_DESCRIPTION_MAX_CHARS} + END + `, status: issues.status, workMode: issues.workMode, harnessKind: issues.harnessKind,