feat(workspace): mid-turn abort + steering for foreman chat #586
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!586
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/564"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
AbortController; the SDK tears down; the task landscancelledintask_history; a syntheticresult { subtype: "aborted" }SSE event clears the client's?task=param and triggers a session refetch. Partial assistant text is persisted viaappendMessagebefore returning.POST /foreman/steer/:task_id. Queued steer pills render above the streaming bubble and flip from pending (accent) to delivered (success) when the SDK consumes the message.queueForemanSteerwas added to bypass theforbidden_foremanguard inqueueSteer, and wired throughForemanAgentDispatch+runForemanTurn(withregisterSteerTask/steerInputIterator+ try/finally cleanup)."cancelled"added toTaskResult.statusand handled inregistry.tsonFinishto bypassresolveShutdownStatus.Closes #564
Test plan
?task=clearscancelledstatus🤖 Generated with Claude Code
- Esc (workspace-level, capture phase) or Stop button aborts the running turn; server flips AbortController, task lands `cancelled` in task_history, and a synthetic `result { subtype: "aborted" }` SSE event clears the client stream. - Partial assistant text is persisted via `appendMessage` before the abort returns. - Queue button (replaces Send while streaming) enqueues a steering message via `POST /foreman/steer/:task_id`; queued pills render above the streaming bubble and flip to "delivered" when the SDK consumes the steer. - `queueForemanSteer` bypasses the `forbidden_foreman` guard in `queueSteer`. - `registerSteerTask` / `steerInputIterator` wired into `runForemanTurn` with try/finally cleanup; `onSteerQueued`/`onSteerDelivered` forwarded through `ForemanAgentDispatch`; `"cancelled"` added to `TaskResult.status`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>7985035437db9a40ff19CI still pending at review time (run #2611, sha
db9a40f). Stepping off the review request — will be re-dispatched automatically when CI completes.All ACs from #564 met; CI green (run #2611).
Abort path is correct:
currentAbort?.abort()→ SDK loop breaks → partial text persisted → syntheticresult { subtype: "aborted" }emitted →cancelledstatus threaded throughTaskResult,registry.tsonFinish, andresolveShutdownStatusbypass.deregisterSteerTaskinfinallyis the right place.Steering path is correct:
queueForemanSteer(noforbidden_foremanguard),steerInputIteratorwired throughForemanAgentDispatch, steer pills build correctly fromsteer_queued/steer_deliveredevents.Two nits not worth blocking:
handleForemanSteer: empty message passesnulltoqueueForemanSteer→ 409 "task not registered" instead of 400 "message required". Unreachable via the UI (client guards!trimmed) but misleading if hit directly.resultSSE clearsstreaming, the secondabortMutation.mutate()gets a 404 and shows an error toast despite the abort having succeeded. Could be guarded withabortMutation.isPending.