Files
PaperClipAI/server
1a7eb70a37 fix(run-dispatch): save a redirect sent while a stopped run settles (#19122)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can stop a running agent and then send a new message that
redirects the work
> - The stopped run keeps settling for some seconds after Stop (backup
and checkpoint persist)
> - A message sent in that window is queued as a normal run before
settlement records the workspace recovery hold
> - The dispatch gate then withholds the unstarted run, but it saved the
message only for sources with a native workspace-sync baseline
> - For other sources (remote workspace, snapshot-only) it marked the
wake `skipped`, so the message was lost and the task stayed in progress
with no run and no wake
> - This pull request saves the withheld message for any source when the
message was written after the Stop
> - The benefit is that a redirect after Stop is acted on exactly once
instead of disappearing

## Linked Issues or Issue Description

**What happened?**
After Stop, a user posted a redirect comment with resume intent. The
follow-up run was created while the stopped run was still persisting its
checkpoint. When settlement finished, the dispatch gate cancelled the
follow-up run with `execution_reconciliation_required` and an
empty-looking error ("Recovering saved work…"). The user's wake was
marked `skipped`. Nothing woke the task again. It stayed `in_progress`
with no active or queued run.

**Expected behavior**
The message sent after Stop runs exactly once after the stopped run
settles. The task never rests in progress with no run and no pending
wake.

**Steps to reproduce**
1. Start a native run whose stopped source has no native workspace-sync
baseline (for example a remote-workspace sandbox run).
2. Stop the run.
3. Post a comment with resume intent while the stopped run still
persists its checkpoint.
4. Let settlement record the workspace recovery hold.
5. Observe that the queued follow-up run is cancelled with
`execution_reconciliation_required` and its wake is `skipped`.

**Root cause**
`deferUnstartedUserComment` in
`server/src/modules/run-dispatch/adapters/postgres.ts` saved a withheld
user comment only when the stopped source had a native workspace-sync
baseline (the copyback replay case). The enqueue path saves or admits
the same message when it arrives after the hold exists. The gap was only
timing.

Refs #18332 (added the native-baseline-only deferral this PR widens).

**Paperclip version or commit**
Reproduced on `master` at e107a75755. The
touched code is unchanged on current `master`.

**Agent adapter(s) involved**
Not adapter-specific (core bug). Seen with the Paperclip runner adapter.

## What Changed

- `deferUnstartedUserComment` now saves a withheld user comment as a
`deferred_issue_execution` receipt for any stopped native source when
the latest saved comment was written after the source's Stop
(`finishedAt`).
- The native-baseline copyback path is unchanged.
- Input written before the Stop keeps the old behavior (`skipped`).
Older queued input cannot undo a Stop.
- Delivery uses the existing deferred-receipt path
(`resumeRemoteStopComments` / `resumeDeferredExecutionReceipts` →
`admitExplicitNativeContinuation` with `queuedCommentRequestId`). All
continuation gates still apply.
- New regression in `workspace-recovery.test.ts` that controls the
timing: the message is queued while the stopped run is still active,
then the hold is recorded, then the gate runs.
- The existing snapshot-only queued-reset tests asserted the dropped
message. They now assert that the message is saved and delivered once,
like the native-baseline case.

## Verification

- Red before the fix: the new test failed with the wake `status:
"skipped"`.
- Green after the fix:
- `cd server && npx vitest run src/services/workspace-recovery.test.ts`
(308 passed)
- `cd server && npx vitest run src/modules/run-dispatch
src/__tests__/heartbeat-undispatched-execution-wait.test.ts
src/__tests__/issue-queued-comments-routes.test.ts
src/services/explicit-native-continuation.test.ts
src/__tests__/heartbeat-process-recovery.test.ts
src/services/execution-continuation.test.ts`
- Safety cases in the new test:
- The redirect is saved, then delivered as exactly one successor run
with the original user's authority. The old provider run is not
executed.
- Input written before the Stop stays skipped and creates no successor.
- The UI Stop button uses the same `POST /heartbeat-runs/:id/cancel`
path. A comment with `interrupt: true` cancels through the same
`cancelRun` and then queues the comment wake. Both reach the same gate.
These were checked by code reading, not by a separate test.
- Not done: a live staging reproduction at the fixed revision.
Deployment is out of scope.

## Risks

- Behavior change: for sources without a native baseline, a post-Stop
message is now saved and delivered after recovery, instead of dropped.
Admission still blocks it until workspace repair is verified and the
previous execution has stopped. While blocked, the task shows "Your
message is saved."
- If admission never succeeds, the message stays saved and visible.
Before, it was silently lost.
- No schema or API change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, file edits, browser for read-only staging diagnostics).

## 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:39:48 -06:00
..