From 5888cbf72afc43224cd9bde57e37e0d32ddb7474 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:56:42 -0500 Subject: [PATCH] fix(issues): restore checkout after accepted confirmations (#10909) 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 > - Issue thread confirmations can pause an issue until a board user makes a decision > - Atomic checkout is the only supported transition into `in_progress` > - An accepted confirmation left a creator-owned issue in `in_review` while it started a continuation worker > - The worker could run without the normal checkout state transition > - This pull request returns that narrow review state to `todo` before it queues the continuation wake > - The benefit is that the worker can check out the issue and move it to `in_progress` through the normal atomic path ## Linked Issues or Issue Description No matching public GitHub issue exists. The following pull requests are related but do not fix this case: - Refs #10376. It handles refusal paths for user-owned issues. - Refs #8516. It handles rejected confirmations for user-owned issues. - Refs #10274. It gives ownerless waking interactions an agent owner. **What happened?** An agent created a confirmation on an issue that was assigned to that same agent and had status `in_review`. A board user accepted the confirmation. Paperclip started a continuation worker, but the issue stayed `in_review`. The normal checkout fields stayed empty. **Expected behavior** Paperclip must return the issue to an actionable state before it wakes the continuation worker. The worker must then use atomic checkout to move the issue to `in_progress`. **Steps to reproduce** 1. Assign an issue to an agent and set the issue status to `in_review`. 2. Let that agent create a `request_confirmation` with `wake_assignee_on_accept`. 3. Accept the confirmation as a board user. 4. Observe that the continuation worker starts while the issue remains `in_review`. **Paperclip version or commit** The bug reproduced on master before this pull request. This branch is based on `ffd62a4cbb`. **Deployment mode** Local development. The server logic is deployment-independent. **Agent adapter(s) involved** Codex exposed the bug, but the issue-thread continuation logic is adapter-independent. **Database mode** The regression test uses embedded PostgreSQL. The logic is database-mode independent. **Access context** An agent creates the confirmation. A board user accepts it. ## What Changed - Allow an accepted agent-authored confirmation to return an agent-owned issue only when the issue is `in_review` and the owner is the creating agent. - Keep active `in_progress` work unchanged so an accepted confirmation cannot reset a running worker to `todo`. - Add embedded-PostgreSQL regression coverage for user-owned review, creator-owned review, and creator-owned active work. ## Verification - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/issue-thread-interactions-service.test.ts --config vitest.config.ts` — 48 passed. - `pnpm -r typecheck` — passed for all workspace projects. - `pnpm build` — passed for all workspace projects. - `pnpm test:run` — 3,411 passed. Three timing-sensitive assertions failed in the unchanged `heartbeat-workspace-busy.test.ts` suite. - Isolated rerun of `heartbeat-workspace-busy.test.ts` — 15 passed. ## Risks Low risk. The behavior change is limited to accepted confirmations on non-terminal `in_review` issues that the creating agent already owns. It does not change active work, blocked work, terminal issues, other agent owners, schemas, or public API contracts. > This is a focused bug fix. It does not add roadmap scope. ## Model Used OpenAI Codex based on GPT-5. The runtime does not expose the exact deployment ID or context-window size. The model used reasoning, repository tools, code editing, Git, and local 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 - [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: Paperclip --- .../issue-thread-interactions-service.test.ts | 86 ++++++++++++++++++- .../src/services/issue-thread-interactions.ts | 8 +- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/server/src/__tests__/issue-thread-interactions-service.test.ts b/server/src/__tests__/issue-thread-interactions-service.test.ts index 7773770910..364d2bbe64 100644 --- a/server/src/__tests__/issue-thread-interactions-service.test.ts +++ b/server/src/__tests__/issue-thread-interactions-service.test.ts @@ -1867,7 +1867,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => { }); }); - it("returns agent-authored request confirmations to the creating agent when a board user accepts", async () => { + it("returns accepted agent confirmations from review without resetting active work", async () => { const companyId = randomUUID(); const goalId = randomUUID(); const issueId = randomUUID(); @@ -1947,6 +1947,90 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => { assigneeAgentId: agentId, assigneeUserId: null, }); + + await db + .update(issues) + .set({ + status: "in_review", + assigneeAgentId: agentId, + assigneeUserId: null, + }) + .where(eq(issues.id, issueId)); + + const agentOwnedConfirmation = await interactionsSvc.create({ + id: issueId, + companyId, + }, { + kind: "request_confirmation", + continuationPolicy: "wake_assignee_on_accept", + payload: { + version: 1, + prompt: "Approve the next step?", + }, + }, { + agentId, + }); + + const resumed = await interactionsSvc.acceptInteraction({ + id: issueId, + companyId, + goalId, + projectId: null, + }, agentOwnedConfirmation.id, {}, { + userId: "local-board", + }); + + expect(resumed.continuationIssue).toEqual({ + id: issueId, + assigneeAgentId: agentId, + assigneeUserId: null, + status: "todo", + }); + + const resumedIssue = (await db.select().from(issues)).find((issue) => issue.id === issueId); + expect(resumedIssue).toMatchObject({ + id: issueId, + status: "todo", + assigneeAgentId: agentId, + assigneeUserId: null, + }); + + await db + .update(issues) + .set({ status: "in_progress" }) + .where(eq(issues.id, issueId)); + + const activeConfirmation = await interactionsSvc.create({ + id: issueId, + companyId, + }, { + kind: "request_confirmation", + continuationPolicy: "wake_assignee_on_accept", + payload: { + version: 1, + prompt: "Approve while work is active?", + }, + }, { + agentId, + }); + + const acceptedWhileActive = await interactionsSvc.acceptInteraction({ + id: issueId, + companyId, + goalId, + projectId: null, + }, activeConfirmation.id, {}, { + userId: "local-board", + }); + + expect(acceptedWhileActive.continuationIssue).toBeNull(); + const activeIssue = (await db.select().from(issues)).find((issue) => issue.id === issueId); + expect(activeIssue).toMatchObject({ + id: issueId, + status: "in_progress", + assigneeAgentId: agentId, + assigneeUserId: null, + }); }); it("expires request confirmations by default when a user comments after creation", async () => { diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts index dd862d9121..ff7f001d2c 100644 --- a/server/src/services/issue-thread-interactions.ts +++ b/server/src/services/issue-thread-interactions.ts @@ -310,10 +310,12 @@ function shouldReturnAcceptedConfirmationToCreatorAgent(args: { if (!isRequestConfirmationLikeKind(args.current.kind)) return false; if (!args.current.createdByAgentId) return false; if (!args.actor.userId) return false; - if (!args.issue.assigneeUserId) return false; - if (args.issue.assigneeAgentId) return false; if (isTerminalIssueStatus(args.issue.status)) return false; - return true; + if (args.issue.assigneeAgentId) { + return args.issue.status === "in_review" + && args.issue.assigneeAgentId === args.current.createdByAgentId; + } + return Boolean(args.issue.assigneeUserId); } function shouldSupersedeInteractionOnUserComment(interaction: UserCommentSupersedableInteraction) {