返回 DeepSeek-Reasonix
cancel.go
根目录 / internal / control / cancel.go
1 package control
2
3 import (
4 "reasonix/internal/agent"
5 "reasonix/internal/event"
6 )
7
8 // CancelReceipt acknowledges a session-scoped Stop request. Accepted means the
9 // cancellation signal was processed; it does not claim that every owned
10 // operation has already exited.
11 type CancelReceipt struct {
12 SessionRef string `json:"sessionRef"`
13 HeadID string `json:"headId"`
14 RuntimeEpoch string `json:"runtimeEpoch"`
15 Accepted bool `json:"accepted"`
16 AlreadyIdle bool `json:"alreadyIdle"`
17 RecoveryRequired bool `json:"recoveryRequired"`
18 }
19
20 // CancelSession stops the activity owned by this captured controller. Callers
21 // do not need a turn id, and an idle cancellation is idempotently successful.
22 func (c *Controller) CancelSession() CancelReceipt {
23 return c.CancelSessionFrom("unknown")
24 }
25
26 // CancelSessionFrom records provenance only; cancellation ownership is unchanged.
27 func (c *Controller) CancelSessionFrom(source string) CancelReceipt {
28 if c == nil {
29 return CancelReceipt{Accepted: true, AlreadyIdle: true}
30 }
31 c.mu.Lock()
32 alreadyIdle := c.turns.cancel == nil && !c.bodyActiveLocked() && !c.finalizingLocked()
33 sessionRef := c.sessionPath
34 c.mu.Unlock()
35 headID := agent.BranchID(sessionRef)
36 c.runtimeState.mu.Lock()
37 epoch := c.runtimeState.snapshot.RuntimeEpoch
38 recoveryRequired := c.runtimeState.snapshot.Phase == "recovery_required"
39 c.runtimeState.mu.Unlock()
40 _, runtime, exclusive := c.v3Binding()
41 if exclusive && runtime != nil {
42 sessionRef = runtime.Ref().SessionID
43 headID = ""
44 }
45 token, turnID, cancelled := c.signalTurnCancelIdentity()
46 c.recordLifecycle("cancel_requested", source, turnID, 0, "")
47 if cancelled {
48 alreadyIdle = false
49 }
50 go c.finishCancellation(token, turnID, cancelled)
51 receipt := CancelReceipt{
52 SessionRef: sessionRef, HeadID: headID, RuntimeEpoch: epoch,
53 Accepted: true, AlreadyIdle: alreadyIdle, RecoveryRequired: recoveryRequired,
54 }
55 c.recordLifecycle("cancel_acknowledged", source, turnID, 0, "")
56 return receipt
57 }
58
59 // Cancel aborts the in-flight turn. A goroutine blocked awaiting approval
60 // unblocks via the cancelled context.
61 func (c *Controller) Cancel() {
62 c.recordLifecycle("cancel_requested", "unknown", "", 0, "")
63 turnID, cancelled := c.cancelTurnLocked()
64 c.finishCancel(turnID, cancelled)
65 }
66
67 // cancelLocked is retained for call sites already inside a typed prompt
68 // transition. Cancellation itself is independent of answer serialization.
69 func (c *Controller) cancelLocked() {
70 turnID, cancelled := c.cancelTurnLocked()
71 c.finishCancel(turnID, cancelled)
72 }
73
74 // cancelTurnLocked signals the turn before any observable work: the status
75 // emit that follows is a synchronous event barrier, and a stalled event lane
76 // must never keep the provider stream or a tool process alive after Stop.
77 func (c *Controller) cancelTurnLocked() (string, bool) {
78 _, turnID, cancelled := c.signalTurnCancelIdentity()
79 if !cancelled {
80 return "", false
81 }
82 c.promptOwner.CancelTurn(turnID)
83 return turnID, true
84 }
85
86 func (c *Controller) finishCancellation(token uint64, turnID string, cancelled bool) {
87 c.mu.Lock()
88 current := c.turns.token == token
89 c.mu.Unlock()
90 if !current {
91 return
92 }
93 c.promptOwner.CancelTurn(turnID)
94 c.finishCancel(turnID, cancelled)
95 }
96
97 func (c *Controller) finishCancel(turnID string, cancelled bool) {
98 defer c.refreshRuntimeState(event.Event{})
99 if cancelled {
100 c.emitTurnStatus(event.TurnCancelling, turnID)
101 }
102 c.mu.Lock()
103 stale := turnID != "" && c.turns.turnID != "" && c.turns.turnID != turnID
104 c.mu.Unlock()
105 if stale {
106 return
107 }
108 if c.goals.active() {
109 c.stopGoal(GoalStatusStopped)
110 }
111 if c.sessionEngineEnabled() {
112 c.disarmGoalLifecycle("cancelled")
113 }
114 }
115
115 lines GO