Files
PaperClipAI/maps/feature-map/steering.md
T
Devin FoleyandPaperclip c9213d1be4 fix: send saved queued messages after Stop (#17959)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People can save follow-up messages while an agent runs.
> - Stop preserves those messages and stops the active execution.
> - The queue removed its delivery controls when that execution ended.
> - Native admission also refused an explicit resend of older queued
input.
> - This change adds Send for a stopped queue and records fresh board
intent.
> - The saved message can continue through normal cleanup and admission
checks.

## Linked Issues or Issue Description

**What happened?**

A person queued a message with a screenshot while an agent was starting.
Steering was not yet available. They stopped the run. The message
remained visible, but no control could send it. Native queue delivery
also rejected this transition.

**Expected behavior**

A stopped queue offers Send. The click preserves message IDs, content,
attachments and original authors. It starts at most one eligible
continuation. Stop remains effective until the person gives new
direction.

**Steps to reproduce**

1. Start a native task and queue a second message.
2. Stop the active run before it consumes that message.
3. Open the task after cleanup. Observe the saved message with no send
control.

Refs #16965 for the queue-control change and #15015 for stopped
conversation recovery. No open repair for this exact transition was
found.

## What Changed

- Show Send for a saved deferred queue with no active run.
- Accept the existing board queue action for a stopped native target.
Keep live native interruption restricted.
- Use the server-recorded click time to authorize older native messages.
Reject stale, malformed and wrong-actor intents.
- Reuse normal queue dispatch, revision checks, cleanup proofs and
execution admission, including a failed native predecessor after a
verified Send.
- Ignore a fully committed, successful recovered export when admitting a
fresh message, while retaining the wait for results still committing.
- Honor an applied `/new` conversation boundary while retaining native
execution holds until the existing stop and cleanup checks verify them.
Workspace holds also survive reset. Check the newest failed or cancelled
native execution even when older recovery actions exist.
- Keep native automatic delivery separate from the explicit Send action.
Update execution and access documentation.

## Verification

- Meaningful regressions failed before the fixes: stopped queues lacked
Send, native admission refused fresh intent, cleanup and dispatch
receipts were missing, deferred rows disappeared, live plan Interrupt
rejected its own receipt, and Agent Chat `/new` inherited an old
non-workspace execution hold.
- Six assembled-wakeup cases fail before the reset cleanup fix. All 11
reset/history cases pass after it: live processes, missing identity,
quarantined cleanup, remote leases and live controllers remain held;
verified cleanup proceeds and retires the hold. Two more cases fail
before the fallback fix and pass after it: Send verifies a stopped or
live native predecessor despite a later reset receipt and no recovery
record.
- **525 server tests pass** across seven queue admission, route,
dispatch, envelope, provenance, blocker and conversation files. **343
queue/thread UI tests** also pass. Server/UI TypeScript, token and map
guards pass. The final scoped server typecheck also builds native Runner
successfully.
- The latest regressions prove that completed recovered exports permit
fresh input, and clearing conversation history still requires old native
stop and cleanup verification; an unsafe workspace still blocks after
`/new`; failed native Send reaches actual continuation dispatch with the
original queued comment. A result still being committed remains
deferred. History plus a newer Stop cannot hide a missing
process/controller/lease receipt.
- The real Chromium Stop/Send journey passes on current head
`75fa5e7222c59f43ff7ac326e81093bc9da28330`: queue a follow-up, Stop,
reload, explicitly Send, then verify exactly one useful response and no
live run. Earlier browser journeys cover legacy Interrupt, interruption
during a write and Stop followed by a new direction. These use an
isolated app/database and controlled ACP fixture, not an external AI
account or a staging deployment.
- Negative cases retain actor, company, issue, assignee, queue and
cancellation-receipt bindings; cleanup/process/workspace/controller
holds; approvals; pause and budget gates. Typed responses keep their
original resolver and immutable result.
- Full local recursive typecheck stopped in unchanged Rust Runner
compilation with ENOSPC, so the chained full tests/build did not run.
All hosted CI gates pass on final commit
`75fa5e7222c59f43ff7ac326e81093bc9da28330`, including typecheck, tests,
build and E2E. Apex review on that commit is 5/5, with no unresolved
findings. Earlier unrelated CI failures involved PostgreSQL
fixture-cleanup deadlock and an unchanged decision-expiry deadline
assertion; the latter's 20 cases pass locally without source changes. No
assertions or limits were weakened. Local resource cleanup was limited
to this task's verified completed test processes and rebuildable npm
caches.

## Risks

- Send can authorize a new turn for a saved message after Stop. A Send
click must be newer than the stopped execution; a live plan Interrupt is
bound to the cancellation receipt produced by that same click. It does
not imply that cleanup finished or that an external action is safe to
replay.
- The existing endpoint and intent format are reused. No schema or
credential change is required. Live native steering and legacy
interruption keep their existing paths.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository inspection, tool
use and code execution. The session does not expose the exact serving
model ID or context window size.

## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-27 20:34:14 -07:00

194 lines
11 KiB
Markdown

---
navigation: {"section":"Tasks and conversations","sectionOrder":20,"order":60}
---
# Steering and queued messages
People can send follow-ups while an agent works, manage saved messages that have
not been consumed, and choose when to steer or interrupt. Stopping a run and
pausing a task are separate controls. The UI must preserve the message, explain
why it is waiting, and only claim delivery after an authoritative acknowledgement.
Feature ID: `steering`.
Agent access: [companion](../agent-access-map/steering.md).
## Sub-features
- `queue`: `steering/queue` — a busy task saves follow-ups and shows their order and wait reason.
- `edit-order-discard`: `steering/edit-order-discard` — edit full Markdown, reorder, and discard eligible saved
messages; ownership, revision conflicts, and already-dispatched states matter.
- `steer`: `steering/steer` — explicit user action sends a queued message into a compatible active
run; unavailable capabilities and provider rejection remain visible.
- `interrupt`: `steering/interrupt` — stop the current execution and carry accepted follow-ups into
the eligible continuation without delivering them twice.
- `stop-and-pause`: `steering/stop-and-pause` — distinguish stop-in-flight, confirmed stop, task/ancestor
pause, agent pause, and budget hold. Resume must respect the actual gate.
- `drafts`: `steering/drafts` — switching tasks, reloading, and uncertain submission retain the
correct task's draft and attachment receipts.
- `approval-queue`: `steering/approval-queue` — answered questions and accepted approvals have durable
delivery distinct from editable ordinary comments.
## How to get to it (user POV)
### `task-composer`
Open an assigned task (`/issues/:issueId`). Start work, then send follow-ups while
the agent runs. Use the queued-message controls to edit, reorder, discard,
steer, or interrupt when offered. After Stop, choose **Send** on a saved queue to explicitly continue it without recreating the messages or attachments. Native steering still requires an active turn; Send after Stop is a separate user decision. Use the composer stop/pause control separately.
Both task presentations need attention when changing their shared behavior.
### `agent-chat`
Enable Agent Chat and open an agent conversation. Send another message during
a reply and inspect the shared queue. `/new` starts fresh provider context in
the same conversation; it is an ordered boundary, not a normal model prompt.
### `request-response`
Answer an earlier question or accept a confirmation while successor work is
active on the task. Inspect the resulting queued response and its available
steer/interrupt control instead of assuming acceptance interrupted execution.
### `external-channel`
Send a follow-up from a linked external chat conversation while its task is
running. Use that provider's supported stop/control affordance where available;
inspect the resulting task and run in Paperclip.
## Driving it
Preconditions: follow the [baseline](./README.md#before-driving-a-journey).
Use an agent that stays busy long enough to inspect the queue. Record the adapter,
native/legacy mode, and advertised steering capability. Test supported and
unsupported steering without treating an absent capability as a product failure.
### `task-composer`
Automated: [queued-message UI](../../ui/src/components/task-chat/TaskChatQueuedMessages.test.tsx)
covers order, failure restoration, unavailable steering, stale revisions, and
discard acknowledgements. [Queue routes](../../server/src/__tests__/issue-queued-comments-routes.test.ts)
cover durable ordering, authorization, steering acknowledgements, interrupt
recovery, and concurrent dispatch. [Composer](../../ui/src/components/task-chat/TaskChatComposer.test.tsx)
covers drafts and uncertain saves.
[Task-detail delivery races](../../ui/src/pages/IssueDetail.test.tsx) cover a
consumed message whose send response and queue snapshot arrive late. A started
run's input receipt removes that message from pending, while a separate unsent
message stays queued. Admission to a run that has not started is not delivery.
A later retry of the same message remains visible in its new authoritative queue.
A queued turn's own starting input stays in history before execution, including
board messages that reopen a task. Later inputs keep their queue controls and
full canonical order. A maintenance hold displays its current admission wait;
drain release and expiry prompt normal admission without a manual retry.
Receipts retain the primary wake and coalesced source queue IDs. They clear a late
snapshot even when its target names the preceding run or is null.
```sh
pnpm exec vitest run ui/src/components/task-chat/TaskChatQueuedMessages.test.tsx server/src/__tests__/issue-queued-comments-routes.test.ts
```
Automated stop coverage is separate:
[composer stop](../../tests/e2e/composer-stop.spec.ts) with
[its configuration](../../tests/e2e/playwright-composer-stop.config.ts) and
[ACP stop/continuation](../../tests/e2e/acp-stop-continuation.spec.ts). Inspect their
fixture and opt-in prerequisites; skipped native cases are not native proof.
The explicit [follow-up/Stop/resume journey](../../tests/runner-e2e/followup-stop-resume-flow.ts)
requires a later user message before continuation. The
[database Stop accounting regression](../../server/src/services/native-runtime/native-stop-accounting.integration.test.ts)
checks complete and late receipts without another provider turn. These are
separate scopes; database success does not establish deployed or Slack acceptance.
Manual: queue three distinguishable messages. Edit one, reorder them, reload,
and confirm the saved order. Discard one and confirm it never reaches execution.
After a queued message enters a started run, confirm it leaves pending even if
the send response arrives later. Reload and confirm it appears once in history.
Steer one while busy and verify the agent receives that exact message once.
Repeat with a rejected steer and a stale second-tab edit; the UI must preserve
retryable content and explain the conflict. On a fresh run, interrupt with a
queued follow-up and verify confirmed stop followed by one continuation. Pause
the task (and separately its ancestor), preserve a draft, then resume the actual
hold; no hidden send should occur while paused. Check both task presentations
when available; the named component tests do not prove every host integration.
Fixture-backed recovery check: start with an agent in its completed paused
lifecycle state, then insert a new accepted `deferred_issue_execution` request
through the [PostgreSQL recovery fixture](../../server/src/modules/wake-queue/adapters/postgres.test.ts).
Normal wake admission
rejects paused agents, so this pending state cannot be created by sending a new
message through the ordinary UI or API. Let release and periodic recovery run.
Confirm that the saved request remains pending. Resume the agent, wait for its
lifecycle to become ready, and verify one delivery. An explicit Pause cancels
older queued work; those cancelled receipts must not be replayed. A paused
request must not prevent another eligible recipient's queued work from proceeding.
### `agent-chat`
Automated: [agent chat sessions](../../tests/e2e/agent-chat-sessions.spec.ts) covers
persistent conversation/session behavior with deterministic harness fixtures;
shared composer tests cover the controls. These do not qualify live steering
for every harness.
Manual: queue two follow-ups while the conversation replies. Reload, edit or
discard one, then let the other run. Verify one reply and unchanged conversation
identity. Send `/new`, then a new prompt; old history remains visible, and the
new turn uses fresh provider context. Verify a useful answer rather than only
the session-divider UI. Repeat the queue operations with a live capable harness
when the change touches delivery.
### `request-response`
Automated: [response delivery](../../server/src/__tests__/question-response-delivery.test.ts)
proves queueing/coalescing and retry-safe receipts. The queue-route suite tests
separate comment and approval queues, explicit steering, and fresh-session
restrictions on plan approvals.
Manual: answer an old question during a new run, confirm that it is saved and
queued, and let the active run end. Verify exactly one continuation with the
answer. Repeat an eligible approval with explicit steer; another approval that
requires a fresh session must not offer same-turn delivery. Do not edit an
approval outcome through an ordinary queued-message editor.
### `external-channel`
Automated: [chat interaction arbitration](../../server/src/services/chat-interaction-arbitration.test.ts)
covers backend arbitration with simulated requests. It is not a provider UI test.
Manual: send two distinguishable messages through one disposable linked
conversation while busy. Inspect their saved task order, run delivery, and
external reply. Check a duplicate provider event and unauthorized sender.
Provider-specific control semantics and real transport timing remain a manual
gap; do not assume browser queue controls are available in every channel.
## Gotchas
- Paperclip's queue is server-owned once saved; it is not Omnigent's unsent
browser draft buffer. A stale edit must not overwrite a consumed message.
- Steer availability depends on the active runtime's capability, not its brand
name. A UI row disappearing optimistically does not prove provider receipt.
- Interrupt, stop, task pause, agent pause, and budget pause have different
effects. A running indicator disappearing does not prove remote work stopped.
- An interrupted run may need verified cleanup before a saved follow-up can
start. Preserve and inspect the wait reason; see [recovery](./recovery.md).
- A healthy idle Agent Chat is waiting for input, not stranded unfinished work.
### Storybook verification
#### `drafts`
Capability ID: `steering/drafts`.
Scope: ui
Storybook: Task-keyed restored draft and attachment receipts; task switch/reload, failed send and uncertain-save recovery.
Production: Retain correct task draft across navigation and reload; inspect authoritative saved receipt before claiming delivery; do not clear storage on every harness mount.
Sources: Source: [TaskChatComposer.tsx](../../ui/src/components/task-chat/TaskChatComposer.tsx), [composer-draft.ts](../../ui/src/lib/composer-draft.ts). Tests: [TaskChatComposer.test.tsx](../../ui/src/components/task-chat/TaskChatComposer.test.tsx), [issue-queued-comments-routes.test.ts](../../server/src/__tests__/issue-queued-comments-routes.test.ts)
#### `approval-queue`
Capability ID: `steering/approval-queue`.
Scope: ui
Storybook: Answered question/accepted approval delivery waiting, eligible steer/interrupt, failed delivery and fresh-session-only approval.
Production: Keep response delivery durable and distinct from editable comments; coalesce once, enforce fresh-session restrictions and deduplicate concurrent/retried continuation.
Sources: Source: [TaskChatQueuedMessages.tsx](../../ui/src/components/task-chat/TaskChatQueuedMessages.tsx), [question-response-delivery.ts](../../server/src/services/question-response-delivery.ts). Tests: [TaskChatQueuedMessages.test.tsx](../../ui/src/components/task-chat/TaskChatQueuedMessages.test.tsx), [question-response-delivery.test.ts](../../server/src/__tests__/question-response-delivery.test.ts), [issue-queued-comments-routes.test.ts](../../server/src/__tests__/issue-queued-comments-routes.test.ts)