fix(server): flag truncated issue descriptions (#4771)

## 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 <sr@samirusani>
This commit is contained in:
Sam 2026-08-17 23:02:01 +02:00 committed by GitHub
parent c1c46f1e4e
commit 14fd8aee36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 0 deletions

View File

@ -793,6 +793,7 @@ export interface Issue {
ancestors?: IssueAncestor[];
title: string;
description: string | null;
descriptionTruncated?: boolean;
status: IssueStatus;
workMode: IssueWorkMode;
priority: IssuePriority;

View File

@ -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);
});
});

View File

@ -3145,6 +3145,12 @@ const issueListSelect = {
)
END
`,
descriptionTruncated: sql<boolean>`
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,