mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
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`.   ## 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:
parent
d0e9cc76f2
commit
772fc92619
18 changed files with 2269 additions and 117 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue