返回 DeepSeek-Reasonix
session_rotation_http.go
根目录 / internal / serve / session_rotation_http.go
1 package serve
2
3 import (
4 "encoding/json"
5 "net/http"
6 "strings"
7
8 "reasonix/internal/control"
9 "reasonix/internal/event"
10 )
11
12 func (s *Server) planDecision(w http.ResponseWriter, r *http.Request) {
13 var body struct {
14 ID string `json:"id"`
15 Action control.PlanDecisionAction `json:"action"`
16 Feedback string `json:"feedback"`
17 }
18 if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.ID) == "" {
19 http.Error(w, "missing id", http.StatusBadRequest)
20 return
21 }
22 if err := s.ctl().ResolvePlanDecisionWithFeedback(body.ID, body.Action, body.Feedback); err != nil {
23 http.Error(w, err.Error(), http.StatusConflict)
24 return
25 }
26 w.WriteHeader(http.StatusNoContent)
27 }
28
29 func (s *Server) plan(w http.ResponseWriter, r *http.Request) {
30 var body struct {
31 On bool `json:"on"`
32 }
33 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
34 http.Error(w, "bad body", http.StatusBadRequest)
35 return
36 }
37 s.ctl().SetPlanMode(body.On)
38 w.WriteHeader(http.StatusNoContent)
39 }
40
41 func (s *Server) clearSession(w http.ResponseWriter, r *http.Request) {
42 s.clearSessionCommand(w, r, false)
43 }
44
45 func (s *Server) clearSessionFromSubmit(w http.ResponseWriter, r *http.Request) {
46 s.clearSessionCommand(w, r, true)
47 }
48
49 func (s *Server) clearSessionCommand(w http.ResponseWriter, r *http.Request, emitNotice bool) {
50 // Clear rotates the session path just like /new, but also removes the old
51 // transcript artifacts. Keep controller mutation and lease rebinding under
52 // one binding lock so remote clients never observe split ownership.
53 s.bindMu.Lock()
54 defer s.bindMu.Unlock()
55 if !s.validateSwitchExpectedLocked(w, r) {
56 return
57 }
58 // A mirrored foreground cannot rotate in place (its write authority is
59 // gone and its artifacts belong to the local writer). Publish a fresh
60 // replacement instead, and keep the mirrored transcript on disk.
61 if s.foregroundMirroredLocked() {
62 s.mirroredForegroundReplacement(w, r, emitNotice, "cleared (mirrored session kept)")
63 return
64 }
65 if err := s.ctl().ClearSession(); err != nil {
66 if control.IsSessionRotationBusy(err) {
67 http.Error(w, err.Error(), http.StatusConflict)
68 return
69 }
70 http.Error(w, err.Error(), http.StatusInternalServerError)
71 return
72 }
73 if ctrl, ok := s.ctl().(*control.Controller); ok {
74 ctrl.EnsureSessionPath()
75 s.setControllerPath(ctrl, ctrl.SessionPath())
76 }
77 s.bc.ResetSessionPath(s.ctl().SessionPath())
78 if err := s.rebindSessionLease(s.ctl().SessionPath()); err != nil {
79 http.Error(w, sessionInUseError(err), http.StatusConflict)
80 return
81 }
82 path := s.ctl().SessionPath()
83 w.Header().Set(sessionPathHeader, path)
84 writeSessionIDHeader(w, s.ctl())
85 s.announceSessionChanged(path, true)
86 if emitNotice {
87 s.bc.Emit(event.Event{Kind: event.Notice, Text: "context cleared", SessionPath: path})
88 }
89 w.WriteHeader(http.StatusNoContent)
90 }
91
92 // mirroredForegroundReplacement swaps the open-but-released foreground
93 // controller for a fresh session. Used by /new and /clear when the current
94 // session was handed to a local writer: the outgoing controller cannot
95 // snapshot or rotate (its write authority is gone), so publish a replacement
96 // the same way a busy switch does and retire the old controller in the
97 // background. The mirrored transcript stays untouched on disk. Callers hold
98 // bindMu and pass the notice text for the newly created session.
99 func (s *Server) mirroredForegroundReplacement(w http.ResponseWriter, r *http.Request, emitNotice bool, noticeText string) {
100 curCtrl, ok := s.ctl().(*control.Controller)
101 if !ok {
102 http.Error(w, "cannot rotate a mirrored session for this controller implementation", http.StatusConflict)
103 return
104 }
105 if err := s.busyDetach(r.Context(), curCtrl, "", nil); err != nil {
106 s.renderBindError(w, err)
107 return
108 }
109 path := s.ctl().SessionPath()
110 w.Header().Set(sessionPathHeader, path)
111 writeSessionIDHeader(w, s.ctl())
112 s.announceSessionChanged(path, true)
113 if emitNotice {
114 s.bc.Emit(event.Event{Kind: event.Notice, Text: noticeText, SessionPath: path})
115 }
116 w.WriteHeader(http.StatusNoContent)
117 }
118
119 func (s *Server) newSession(w http.ResponseWriter, r *http.Request) {
120 s.newSessionCommand(w, r, false)
121 }
122
123 func (s *Server) newSessionFromSubmit(w http.ResponseWriter, r *http.Request) {
124 s.newSessionCommand(w, r, true)
125 }
126
127 func (s *Server) newSessionCommand(w http.ResponseWriter, r *http.Request, emitNotice bool) {
128 s.bindMu.Lock()
129 defer s.bindMu.Unlock()
130 if !s.validateSwitchExpectedLocked(w, r) {
131 return
132 }
133 cur := s.ctl()
134 if controllerHasActiveRuntimeWork(cur) {
135 curCtrl, ok := cur.(*control.Controller)
136 if !ok {
137 http.Error(w, "cannot start a new session while active work or background jobs are running", http.StatusConflict)
138 return
139 }
140 if err := s.busyDetach(r.Context(), curCtrl, "", nil); err != nil {
141 s.renderBindError(w, err)
142 return
143 }
144 path := s.ctl().SessionPath()
145 w.Header().Set(sessionPathHeader, path)
146 writeSessionIDHeader(w, s.ctl())
147 s.announceSessionChanged(path, true)
148 if emitNotice {
149 s.bc.Emit(event.Event{Kind: event.Notice, Text: "new session", SessionPath: path})
150 }
151 w.WriteHeader(http.StatusNoContent)
152 return
153 }
154 // A mirrored foreground cannot NewSession() in place — the rotation
155 // snapshots the old session first, which fails closed without write
156 // authority. Publish a fresh replacement controller instead.
157 if s.foregroundMirroredLocked() {
158 s.mirroredForegroundReplacement(w, r, emitNotice, "new session")
159 return
160 }
161 if err := cur.NewSession(); err != nil {
162 if control.IsSessionRotationBusy(err) {
163 http.Error(w, err.Error(), http.StatusConflict)
164 return
165 }
166 http.Error(w, err.Error(), http.StatusInternalServerError)
167 return
168 }
169 if ctrl, ok := cur.(*control.Controller); ok {
170 ctrl.EnsureSessionPath()
171 s.setControllerPath(ctrl, ctrl.SessionPath())
172 }
173 s.bc.ResetSessionPath(cur.SessionPath())
174 if err := s.rebindSessionLease(cur.SessionPath()); err != nil {
175 http.Error(w, sessionInUseError(err), http.StatusConflict)
176 return
177 }
178 path := cur.SessionPath()
179 w.Header().Set(sessionPathHeader, path)
180 writeSessionIDHeader(w, cur)
181 s.announceSessionChanged(path, true)
182 if emitNotice {
183 s.bc.Emit(event.Event{Kind: event.Notice, Text: "new session", SessionPath: path})
184 }
185 w.WriteHeader(http.StatusNoContent)
186 }
187
187 lines GO