From 23e7ee9a2d2b7f23dd0d22d6c858d7abe915cbd8 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Oct 2026 23:36:48 +0200 Subject: [PATCH] fix(issues): exclude terminal children from helper cap (#11542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The issue service lets agents create bounded child work through a helper. > - The helper counted completed and cancelled children against its safety limit. > - A recurring coordinator could reach the limit even when almost all historical children were terminal. > - The safety limit must still bound current delegated work. > - This pull request counts only non-terminal children when it applies the limit. > - The benefit is that recurring parents can keep their lineage without losing the active-work guardrail. ## Linked Issues or Issue Description **What happened?** The child-creation helper rejected a new child after a parent had 25 historical children. It counted children with `done` and `cancelled` status. **Expected behavior** The helper must apply its limit to non-terminal children. Terminal children must remain attached to the parent for lineage and audit history. **Steps to reproduce** 1. Create one parent issue. 2. Create 25 child issues and move them to `done` or `cancelled`. 3. Use the issue-service child helper to create another child. 4. Observe that the current code rejects the new child. **Paperclip version or commit** `1a17cbf23` ## What Changed - Exclude `done` and `cancelled` children from the helper cap query. - Keep the limit at 25 non-terminal children. - Add a regression test with terminal history and the active limit boundary. - Clarify the limit error so it says `active child issues`. ## Verification - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "counts only non-terminal children toward the helper child limit"` passed: 1 test passed and 118 skipped. - `pnpm -r typecheck` passed. - `pnpm build` passed. - `pnpm test:run` completed the general server and UI groups successfully. The CLI group then reported one unrelated environment failure because port 3199 was already in use. The failing test was `doctor > re-runs repairable checks so repaired failures do not remain blocking`. ```test-evidence: sha: 071edbf01959744079b8ecc3dc754daa62fe0f58 command: pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "counts only non-terminal children toward the helper child limit" exit: 0 totals: 1 test passed; 118 skipped; 1 test file passed worktree: clean before and after; local == remote == PR head finished_at: 2026-08-17T14:03:53Z ``` ## Risks - Low risk. The change affects only the helper-specific count query. - The helper still rejects creation when 25 non-terminal children exist. - No schema or migration changes are present. > 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 GPT-5 Codex with reasoning, repository tools, code execution, and test execution. ## 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 not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] 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: Codex Engineer Co-authored-by: Paperclip Co-authored-by: commitperclip-bot <305974387+commitperclip-bot@users.noreply.github.com> --- server/src/__tests__/issues-service.test.ts | 57 +++++++++++++++++++++ server/src/services/issues.ts | 3 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 5e4df792486..daa556d4231 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -42,6 +42,7 @@ import { instanceSettingsService } from "../services/instance-settings.ts"; import { clampIssueListLimit, deriveIssueCommentRunLogAttribution, + MAX_CHILD_ISSUES_CREATED_BY_HELPER, ISSUE_LIST_MAX_LIMIT, issueService, readIssueCommentRunLogText, @@ -3922,6 +3923,62 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { expect(child.requestDepth).toBe(MAX_ISSUE_REQUEST_DEPTH); }); + + it("counts only non-terminal children toward the helper child limit", async () => { + const companyId = randomUUID(); + const parentIssueId = 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: parentIssueId, + companyId, + title: "Recurring coordinator", + status: "in_progress", + priority: "high", + }); + + const terminalChildren = Array.from( + { length: MAX_CHILD_ISSUES_CREATED_BY_HELPER }, + (_, index) => ({ + id: randomUUID(), + companyId, + parentId: parentIssueId, + title: `Terminal child ${index + 1}`, + status: index % 2 === 0 ? "done" as const : "cancelled" as const, + priority: "medium" as const, + }), + ); + const activeChildren = Array.from( + { length: MAX_CHILD_ISSUES_CREATED_BY_HELPER - 1 }, + (_, index) => ({ + id: randomUUID(), + companyId, + parentId: parentIssueId, + title: `Active child ${index + 1}`, + status: "todo" as const, + priority: "medium" as const, + }), + ); + await db.insert(issues).values([...terminalChildren, ...activeChildren]); + + const { issue: finalActiveChild } = await svc.createChild(parentIssueId, { + title: "Final active child", + status: "todo", + }); + + expect(finalActiveChild.parentId).toBe(parentIssueId); + await expect(svc.createChild(parentIssueId, { + title: "Over active limit", + status: "todo", + })).rejects.toThrow( + `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} active child issues for this helper`, + ); + }); }); describeEmbeddedPostgres("issueService blockers and dependency wake readiness", () => { diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index c2415b178dc..584c483612f 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -9394,11 +9394,12 @@ export function issueService(db: Db) { and( eq(issues.companyId, parent.companyId), eq(issues.parentId, parent.id), + notInArray(issues.status, ["done", "cancelled"]), ), ); if (childCount >= MAX_CHILD_ISSUES_CREATED_BY_HELPER) { throw unprocessable( - `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} child issues for this helper`, + `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} active child issues for this helper`, ); }