Files
PaperClipAI/server
Devin FoleyandClaude Opus 5.5 ad5cd785b1 fix(wake-queue): keep promoted Retry runs scoped to their issue (#18876)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The wake queue starts the next run for a task when the task's
current run releases its execution lock.
> - A board Retry saves a receipt with only `{ issueId, taskRetry }`.
This receipt has no saved wake context.
> - When the release drain promotes that receipt, the promoted context
gets `taskKey` from the payload, but it does not get `issueId` or
`taskId`.
> - The new run takes the task's execution lock, but the database saves
it as a company-scoped run with no issue.
> - Low-trust workspace preparation then has no issue reference. The run
fails before provider work with "Low-trust execution issue is outside
the active trust boundary."
> - This pull request restores the issue scope of a payload-only receipt
during promotion.
> - The benefit is that a Retry on a low-trust task runs in its task
boundary and does not fail during setup.

## Linked Issues or Issue Description

**What happened?**
A user clicked Retry on a low-trust task while a run was busy. When the
busy run ended, the release drain promoted the saved Retry receipt. The
promoted run had `issue_id = NULL`, `scope_kind = company`, and a
context snapshot with only `taskKey`, `wakeReason: task_retry`,
`wakeSource`, and `wakeTriggerDetail`. Workspace preparation failed with
`low_trust_boundary_mismatch` (HTTP 422) in the setup phase.

**Expected behavior**
A promoted run for a task must keep that task's issue scope. This is
true also when the deferred receipt has no saved wake context. Workspace
preparation must then evaluate the task's own trust boundary.

**Steps to reproduce**
1. Use a task with a low-trust execution policy whose assignee is an
agent.
2. While a run on that task is busy, click Retry on the task.
3. Let the busy run end. The release drain promotes the deferred Retry
receipt.
4. The promoted run is company-scoped with no `issueId`. It fails in
setup with `low_trust_boundary_mismatch`.

**Paperclip version or commit**
`master` at 51d1d4ce07.

**Agent adapter(s) involved**
Not adapter-specific (core bug).

## What Changed

- `enrichPromotedWakeContext`
(`server/src/modules/wake-queue/domain/context.ts`) now restores
`issueId` and `taskId` from the payload when the deferred context does
not have them. This is the same rule that the original wake enrichment
uses. The function does not overwrite a value that the queued context
already has.
- I added a domain test for the payload-only receipt and for the
no-overwrite rule.
- I added a Postgres adapter test
(`adapters/promoted-retry-scope.test.ts`). It runs the real release
drain with a Retry receipt that has no saved wake context. It checks
that the promoted run is issue-scoped and holds the task's execution
lock. The database trigger `set_heartbeat_run_scope_kind` derives
`issue_id` and `scope_kind` from the restored context `issueId`.

## Verification

- Red before the fix: both new tests fail on unchanged `master`. The
adapter test shows the incident shape: `issueId: null`, `scopeKind:
"company"`, and `taskKey` equal to the issue UUID.
- Green after the fix:
- `pnpm vitest run server/src/modules/wake-queue/` passes (10 files, 289
tests).
- The related suites pass (11 files, 468 tests): `heartbeat/queue`,
`heartbeat/run-control`, `heartbeat/run-lifecycle`,
`deferred-execution-receipt-work`, `execution-lock-orphan-cleanup`,
`heartbeat-comment-wake-batching`, `issue-stale-execution-lock-routes`,
`chat-failed-run-retry-boundary`, `task-retry`,
`execution-control-work`, and `workspace-recovery`.
- Server typecheck (`node server/scripts/typecheck-layers.mjs`) passes.
- I did not run a live low-trust Retry on a deployed stack. The adapter
test uses the real release drain, the real database trigger, and the
persisted receipt shape that `requestTaskRetry` writes.

## Risks

- Low risk. The change only adds `issueId` and `taskId` when they are
missing and the payload has them. Deferred wakes that already have a
queued context do not change.
- The release drain still promotes a board Retry receipt directly. It
does not go through `resumeTaskRetry`. This PR does not change that
path. A promoted Retry therefore does not get `forceFreshSession` or
`taskRetryRequestId`. This behavior existed before this PR. It is a
possible follow-up, not part of this fix.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude
Code with tool use (code search, shell, and 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:57:44 -08:00
..