fix(issues): restore checkout after accepted confirmations (#10909)
## 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 <noreply@paperclip.ing>
This commit is contained in:
parent
8ac2526811
commit
5888cbf72a
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue