fix(server): preserve terminal status on issue release (#7524)
Fixes #4206 ## Thinking Path > - Paperclip orchestrates AI agents on issues with checkout/release semantics for execution locks > - `POST /api/issues/:id/release` clears checkout and execution locks when a heartbeat ends without finishing the issue > - `issues.release()` unconditionally set `status: "todo"`, undoing terminal and waiting states (`done`, `cancelled`, `in_review`, `blocked`) set during the session > - Agents reported status drift after release (e.g. `in_review` → `todo`, `done` → `todo`), forcing manual PATCH recovery and risking silent stalls > - This pull request gates the `todo` re-queue to `in_progress` issues only and preserves all other statuses on release > - The benefit is lock cleanup without destroying workflow state agents already recorded ## Linked Issues or Issue Description - Fixes #4206 — `issues.release()` must not downgrade terminal/waiting statuses - Related internal incident: AIT-114 status drift on terminal issue release (AI Trading Council) ## What Changed - `server/src/services/issues.ts` — `releaseStatus` is `todo` only when `existing.status === "in_progress"`; otherwise preserves `existing.status` - `server/src/__tests__/issue-stale-execution-lock-routes.test.ts` — regression tests: release preserves done, cancelled, in_review, blocked keeps `done` and clears lock fields - `server/package.json` — patch bump `0.3.1` → `0.3.2` - `server/CHANGELOG.md` — documents the fix ## Verification ```sh pnpm --filter @paperclipai/server test issue-stale-execution-lock-routes ``` - 7/7 tests pass (parametrized done, cancelled, in_review, blocked) (includes new `preserves terminal status when releasing a done issue` and existing `in_progress` → `todo` on release) - CI: Build, Typecheck, serialized server suites, e2e, Canary Dry Run green on latest head `f31b55f` ## Risks Low risk. Behaviour change is intentional: non-`in_progress` releases no longer force `todo`. Agents that relied on release to re-queue `in_review`/`blocked` work must PATCH status explicitly (documented in agent lifecycle guidance). Rollback: revert this commit and redeploy `@paperclipai/server` 0.3.1. ## Model Used Anthropic Claude Opus 4.6 (extended thinking mode) — 200K context window, tool use enabled. Assisted implementation and PR packaging for AI Trading Council upstream port from local hotfix. ## 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) - [x] I have updated relevant documentation to reflect my changes (CHANGELOG) - [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 (re-review requested on head `f31b55f`) - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: brandon <brandonburr@gmail.com>
This commit is contained in:
parent
8e856d2518
commit
1ba79d82a5
|
|
@ -171,6 +171,59 @@ describeEmbeddedPostgres("stale issue execution lock routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ status: "done" as const, title: "Done release preserves status", completedAt: new Date() },
|
||||
{ status: "cancelled" as const, title: "Cancelled release preserves status", cancelledAt: new Date() },
|
||||
{ status: "in_review" as const, title: "In review release preserves status" },
|
||||
{ status: "blocked" as const, title: "Blocked release preserves status" },
|
||||
])(
|
||||
"preserves $status when releasing a non-in_progress issue",
|
||||
async ({ status, title, completedAt, cancelledAt }) => {
|
||||
const { companyId, agentId, currentRunId } = await seedCompanyAgentAndRuns();
|
||||
const issueId = randomUUID();
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
title,
|
||||
status,
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
checkoutRunId: currentRunId,
|
||||
executionRunId: currentRunId,
|
||||
executionAgentNameKey: "codexcoder",
|
||||
executionLockedAt: new Date(),
|
||||
...(completedAt ? { completedAt } : {}),
|
||||
...(cancelledAt ? { cancelledAt } : {}),
|
||||
});
|
||||
|
||||
const res = await request(createApp(agentActor(companyId, agentId, currentRunId)))
|
||||
.post(`/api/issues/${issueId}/release`)
|
||||
.send();
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
expect(res.body.status).toBe(status);
|
||||
|
||||
const row = await db
|
||||
.select({
|
||||
status: issues.status,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
checkoutRunId: issues.checkoutRunId,
|
||||
executionRunId: issues.executionRunId,
|
||||
executionLockedAt: issues.executionLockedAt,
|
||||
})
|
||||
.from(issues)
|
||||
.where(eq(issues.id, issueId))
|
||||
.then((rows) => rows[0]);
|
||||
expect(row).toEqual({
|
||||
status,
|
||||
assigneeAgentId: null,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
executionLockedAt: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("allows the rightful assignee to release after the owning run failed", async () => {
|
||||
const { companyId, agentId, failedRunId, currentRunId } = await seedCompanyAgentAndRuns();
|
||||
const issueId = randomUUID();
|
||||
|
|
|
|||
|
|
@ -7158,10 +7158,12 @@ export function issueService(db: Db) {
|
|||
}
|
||||
}
|
||||
|
||||
// Release clears checkout/assignee locks; only in_progress work re-queues to todo.
|
||||
const releaseStatus = existing.status === "in_progress" ? "todo" : existing.status;
|
||||
const updated = await tx
|
||||
.update(issues)
|
||||
.set({
|
||||
status: "todo",
|
||||
status: releaseStatus,
|
||||
assigneeAgentId: null,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue