Add issue controls and retry-now recovery (#5426)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Issue operators need clear controls for execution settings, model
overrides, and recovery retries
> - Existing issue properties hid useful adapter override state and did
not expose a board-triggered retry for scheduled heartbeat recovery
> - Scheduled retries also need to respect the same safety gates as
normal execution instead of bypassing budget, review, pause, dependency,
or terminal-state checks
> - This pull request adds the issue property controls and retry-now
surfaces together because they share the issue details/properties UI
> - The benefit is that operators can inspect and adjust issue execution
settings and safely trigger pending scheduled recovery without hidden
control-plane behavior

## What Changed

- Adds editable issue assignee model override controls in
`IssueProperties`, with focused coverage.
- Removes the stale workspace tasks link from issue properties.
- Adds a scheduled retry `retry-now` backend path and shared response
types.
- Adds main-pane and properties-pane scheduled retry UI, backed by a
shared `useRetryNowMutation` hook.
- Adds suppression coverage for budget hard stops, review participant
changes, subtree pause holds, unresolved blockers, terminal issues, and
company scoping.
- Updates the `IssueProperties` test harness with toast actions required
by the retry-now hook.

## Verification

- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueScheduledRetryCard.test.tsx` — 31 passed.
- `pnpm exec vitest run
server/src/__tests__/issue-scheduled-retry-routes.test.ts` — exited 0,
but this host skipped the embedded Postgres route tests with: `Postgres
init script exited with code null. Please check the logs for extra info.
The data directory might already exist.`
- Pairwise merge check against the assigned-backlog PR branch completed
without conflicts via `git merge --no-commit --no-ff` in a temporary
worktree.

### Visual verification screenshots

Storybook story: `Product/Issue Scheduled retry surfaces /
ScheduledRetrySurfaces`.

![Scheduled retry card and issue properties rows -
desktop](https://raw.githubusercontent.com/paperclipai/paperclip/62fb566f357312b43b9162af02252d0175530a8f/docs/assets/pr-5426/scheduled-retry-story-desktop.png)

![Scheduled retry card and issue properties rows -
mobile](https://raw.githubusercontent.com/paperclipai/paperclip/62fb566f357312b43b9162af02252d0175530a8f/docs/assets/pr-5426/scheduled-retry-story-mobile.png)

## Risks

- Medium: this touches issue execution/retry behavior, so CI should run
the embedded Postgres route tests on a host that can initialize
Postgres.
- Low-to-medium UI risk around duplicated retry-now entry points; both
surfaces share one mutation hook to keep behavior consistent.

> 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 Codex coding agent, GPT-5 model family (`gpt-5`), tool-enabled
Paperclip heartbeat environment. Context window and internal reasoning
mode are not exposed by the runtime.

## 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 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
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-05-07 12:23:13 -05:00 committed by GitHub
parent d0e9cc76f2
commit 772fc92619
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2269 additions and 117 deletions

View file

@ -1498,6 +1498,7 @@ export function issueRoutes(
relations,
blockerAttention,
productivityReview,
scheduledRetry,
attachments,
continuationSummary,
currentExecutionWorkspace,
@ -1510,6 +1511,7 @@ export function issueRoutes(
svc.getRelationSummaries(issue.id),
svc.listBlockerAttention(issue.companyId, [issue]).then((map) => map.get(issue.id) ?? null),
svc.listProductivityReviews(issue.companyId, [issue.id]).then((map) => map.get(issue.id) ?? null),
svc.getCurrentScheduledRetry(issue.id),
svc.listAttachments(issue.id),
documentsSvc.getIssueDocumentByKey(issue.id, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY),
currentExecutionWorkspacePromise,
@ -1525,6 +1527,7 @@ export function issueRoutes(
workMode: issue.workMode,
...(blockerAttention ? { blockerAttention } : {}),
productivityReview,
scheduledRetry,
priority: issue.priority,
projectId: issue.projectId,
goalId: goal?.id ?? issue.goalId,
@ -1606,6 +1609,7 @@ export function issueRoutes(
productivityReview,
referenceSummary,
successfulRunHandoffStates,
scheduledRetry,
] = await Promise.all([
resolveIssueProjectAndGoal(issue),
svc.getAncestors(issue.id),
@ -1616,6 +1620,7 @@ export function issueRoutes(
svc.listProductivityReviews(issue.companyId, [issue.id]).then((map) => map.get(issue.id) ?? null),
issueReferencesSvc.listIssueReferenceSummary(issue.id),
listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id]),
svc.getCurrentScheduledRetry(issue.id),
]);
const mentionedProjects = mentionedProjectIds.length > 0
? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
@ -1631,6 +1636,7 @@ export function issueRoutes(
...(blockerAttention ? { blockerAttention } : {}),
productivityReview,
successfulRunHandoff: successfulRunHandoffStates.get(issue.id) ?? null,
scheduledRetry,
blockedBy: relations.blockedBy,
blocks: relations.blocks,
relatedWork: referenceSummary,
@ -2438,6 +2444,44 @@ export function issueRoutes(
res.json({ ok: true });
});
router.post("/issues/:id/scheduled-retry/retry-now", async (req, res) => {
assertBoard(req);
const id = req.params.id as string;
const issue = await svc.getById(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
assertCompanyAccess(req, issue.companyId);
const actor = getActorInfo(req);
const result = await heartbeat.retryScheduledRetryNow({
issueId: issue.id,
actor: {
actorType: actor.actorType,
actorId: actor.actorId,
},
});
await logActivity(db, {
companyId: issue.companyId,
actorType: actor.actorType,
actorId: actor.actorId,
action: "issue.scheduled_retry_retry_now",
entityType: "issue",
entityId: issue.id,
agentId: result.scheduledRetry?.agentId ?? issue.assigneeAgentId ?? null,
runId: result.scheduledRetry?.runId ?? null,
details: {
outcome: result.outcome,
message: result.message,
scheduledRetry: result.scheduledRetry,
},
});
res.json(result);
});
router.patch("/issues/:id", validate(updateIssueRouteSchema), async (req, res) => {
const id = req.params.id as string;
const existing = await svc.getById(id);