mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
fix(issues): exclude terminal children from helper cap (#11542)
## 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: 071edbf019
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 <agent+codex-engineer@agents.techwright.io>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: commitperclip-bot <305974387+commitperclip-bot@users.noreply.github.com>
This commit is contained in:
2 files changed
+59
-1
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user