返回 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.recordLifecycle("cancel_received", source, "", 0, "")
32 c.mu.Lock()
33 alreadyIdle := c.turns.cancel == nil && !c.bodyActiveLocked() && !c.finalizingLocked() && c.maintenance == nil
34 sessionRef := c.sessionPath
35 c.mu.Unlock()
36 headID := agent.BranchID(sessionRef)
37 // Receipt metadata must not wait for a producer that is itself stalled on
38 // execution. Stop signals the owner independently of state observation.
39 state := c.PublishedRuntimeStateSnapshot()
40 _, runtime, exclusive := c.v3Binding()
41 if exclusive && runtime != nil {
42 sessionRef = runtime.Ref().SessionID
43 headID = ""
44 }
45 maintenanceID, maintenancePresent, _ := c.signalMaintenanceCancel()
46 var token uint64
47 var turnID string
48 var cancelled bool
49 if maintenancePresent {
50 turnID = maintenanceID
51 alreadyIdle = false
52 } else {
53 token, turnID, cancelled = c.signalTurnCancelIdentity()
54 }
55 c.recordLifecycle("cancel_requested", source, turnID, 0, "")
56 if cancelled {
57 alreadyIdle = false
58 }
59 if !maintenancePresent {
60 go c.finishCancellation(token, turnID, cancelled)
61 }
62 receipt := CancelReceipt{
63 SessionRef: sessionRef, HeadID: headID, RuntimeEpoch: state.RuntimeEpoch,
64 Accepted: true, AlreadyIdle: alreadyIdle, RecoveryRequired: state.Phase == "recovery_required",
65 }
66 c.recordLifecycle("cancel_acknowledged", source, turnID, 0, "")
67 return receipt
68 }
69
70 // Cancel aborts the in-flight turn. A goroutine blocked awaiting approval
71 // unblocks via the cancelled context.
72 func (c *Controller) Cancel() {
73 c.recordLifecycle("cancel_requested", "unknown", "", 0, "")
74 if _, present, _ := c.signalMaintenanceCancel(); present {
75 return
76 }
77 turnID, cancelled := c.cancelTurnLocked()
78 c.finishCancel(turnID, cancelled)
79 }
80
81 // cancelLocked is retained for call sites already inside a typed prompt
82 // transition. Cancellation itself is independent of answer serialization.
83 func (c *Controller) cancelLocked() {
84 turnID, cancelled := c.cancelTurnLocked()
85 c.finishCancel(turnID, cancelled)
86 }
87
88 // cancelTurnLocked signals the turn before any observable work: the status
89 // emit that follows is a synchronous event barrier, and a stalled event lane
90 // must never keep the provider stream or a tool process alive after Stop.
91 func (c *Controller) cancelTurnLocked() (string, bool) {
92 _, turnID, cancelled := c.signalTurnCancelIdentity()
93 if !cancelled {
94 return "", false
95 }
96 c.promptOwner.CancelTurn(turnID)
97 return turnID, true
98 }
99
100 func (c *Controller) finishCancellation(token uint64, turnID string, cancelled bool) {
101 c.mu.Lock()
102 current := c.turns.token == token
103 c.mu.Unlock()
104 if !current {
105 return
106 }
107 c.promptOwner.CancelTurn(turnID)
108 c.finishCancel(turnID, cancelled)
109 }
110
111 func (c *Controller) finishCancel(turnID string, cancelled bool) {
112 defer c.refreshRuntimeState(event.Event{})
113 if cancelled {
114 c.emitTurnStatus(event.TurnCancelling, turnID)
115 }
116 c.mu.Lock()
117 stale := turnID != "" && c.turns.turnID != "" && c.turns.turnID != turnID
118 c.mu.Unlock()
119 if stale {
120 return
121 }
122 if c.goals.active() {
123 c.stopGoal(GoalStatusStopped)
124 }
125 if c.sessionEngineEnabled() {
126 c.disarmGoalLifecycle("cancelled")
127 }
128 }
129
129 lines GO