| 1 | package control |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // InboxCancelResult is the authoritative receipt for a cancel+withdraw |
| 6 | // operation. Only IDs in DiscardedItemIDs are safe for a frontend to restore |
| 7 | // into its draft. |
| 8 | type InboxCancelResult struct { |
| 9 | DiscardedItemIDs []string |
| 10 | Warning string |
| 11 | } |
| 12 | |
| 13 | // CancelWithInboxItems stops the active turn and discards only the durable |
| 14 | // pending items explicitly owned by the cancelling frontend. Admission is |
| 15 | // paused around the batch deletion so TurnDone cannot race a cancelled item |
| 16 | // into a new provider turn. Unrelated inbox items remain intact. |
| 17 | func (c *Controller) CancelWithInboxItems(ids []string, source string) error { |
| 18 | _, err := c.CancelWithInboxItemsResult(ids, source) |
| 19 | return err |
| 20 | } |
| 21 | |
| 22 | // CancelWithInboxItemsResult serializes withdrawal against every inbox |
| 23 | // admission path and returns exactly the durable messages that were removed. |
| 24 | // A consumed/running item is intentionally absent from the receipt. |
| 25 | func (c *Controller) CancelWithInboxItemsResult(ids []string, source string) (InboxCancelResult, error) { |
| 26 | result := InboxCancelResult{DiscardedItemIDs: []string{}} |
| 27 | c.inbox.admissionMu.Lock() |
| 28 | defer c.inbox.admissionMu.Unlock() |
| 29 | // Capture and signal the foreground owner before touching the inbox store. |
| 30 | // A blocked sidecar or filesystem cannot delay Stop reaching the model/tool |
| 31 | // context. Status persistence and Goal pausing run after the inbox mutation. |
| 32 | turnID, cancelled := c.cancelTurnLocked() |
| 33 | c.recordLifecycle("cancel_requested", source, turnID, 0, "") |
| 34 | defer c.recordLifecycle("cancel_acknowledged", source, turnID, 0, "") |
| 35 | defer c.finishCancel(turnID, cancelled) |
| 36 | st, err := c.ensureInbox() |
| 37 | if err != nil { |
| 38 | return result, err |
| 39 | } |
| 40 | wasPaused := st.Snapshot().Paused |
| 41 | if err := st.SetPaused(true); err != nil { |
| 42 | return result, err |
| 43 | } |
| 44 | discarded, err := st.DiscardPendingItemsOwnedResult(ids, strings.TrimSpace(source)) |
| 45 | if err != nil { |
| 46 | // Keep the inbox paused for inspection if an item already crossed the |
| 47 | // admission boundary. Cancellation still stops that in-flight turn. |
| 48 | return result, err |
| 49 | } |
| 50 | result.DiscardedItemIDs = discarded |
| 51 | if !wasPaused { |
| 52 | if err := st.SetPaused(false); err != nil { |
| 53 | result.Warning = "The turn was stopped, but the message queue remains paused. Review it before resuming." |
| 54 | return result, nil |
| 55 | } |
| 56 | } |
| 57 | return result, nil |
| 58 | } |
| 59 |