返回 DeepSeek-Reasonix
status_http.go
根目录 / internal / serve / status_http.go
1 package serve
2
3 import (
4 "log/slog"
5 "net/http"
6 "path/filepath"
7 "strings"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/config"
11 "reasonix/internal/control"
12 "reasonix/internal/store"
13 )
14
15 // status returns a combined status snapshot. The desktop's runtime-only path
16 // skips provider balance IO while retaining all reconciliation fields.
17 func (s *Server) status(w http.ResponseWriter, r *http.Request) {
18 identityRoute, answered := s.statusIdentityRoute(w, r)
19 if answered {
20 return
21 }
22 if s.writeSpectatorStatus(w, r) {
23 return
24 }
25 s.writeForegroundStatus(w, r, identityRoute)
26 }
27
28 // statusIdentityRoute answers a spectator watching a final-format identity a
29 // local runtime owns. A foreground-bound identity falls through, but the
30 // selector is remembered so the snapshot can state ownership explicitly: a
31 // spectator pin must clear the moment this serve owns the identity again.
32 func (s *Server) statusIdentityRoute(w http.ResponseWriter, r *http.Request) (route string, answered bool) {
33 raw := r.URL.Query().Get("session")
34 if !isSessionIDRoute(raw) {
35 return "", false
36 }
37 if s.statusIdentityOverride(w, raw) {
38 return "", true
39 }
40 return strings.TrimSpace(raw), false
41 }
42
43 // writeSpectatorStatus answers an explicitly selected legacy session with the
44 // file-backed read-only view instead of the foreground controller's.
45 func (s *Server) writeSpectatorStatus(w http.ResponseWriter, r *http.Request) bool {
46 raw := r.URL.Query().Get("session")
47 if raw == "" {
48 return false
49 }
50 path, err := s.resolveSessionPath(raw)
51 if err != nil {
52 return false
53 }
54 held := s.sessionMirrored(path) || leaseHeldByForeignRuntime(path)
55 if !held {
56 if view, ok := s.ownedRuntimeStatusView(path); ok {
57 writeJSON(w, view)
58 return true
59 }
60 }
61 writeJSON(w, s.statusViewForPath(path, held))
62 if s.sessionMirrored(path) {
63 s.maybeAutoReclaimMirrored(path)
64 }
65 return true
66 }
67
68 func (s *Server) writeForegroundStatus(w http.ResponseWriter, r *http.Request, identityRoute string) {
69 // Session rotations publish the controller path and executor Session while
70 // holding bindMu. Read the combined snapshot in that same binding epoch so
71 // callers can never pair a newly published path with the outgoing history.
72 s.bindMu.Lock()
73 ctrl := s.ctl()
74 sess := sessionStatusFields(ctrl)
75 sessionPath := strings.TrimSpace(ctrl.SessionPath())
76 if identity, ok := ctrl.(control.IdentityLifecycle); ok {
77 if ref, bound := identity.SessionRef(); bound {
78 sess["hostId"] = ref.HostID
79 sess["sessionId"] = ref.SessionID
80 }
81 }
82 if identityRoute != "" {
83 // The poller selected an identity explicitly: answer ownership both
84 // ways. Without an explicit false after a reclaim, clients that only
85 // apply present fields keep a stale spectator pin forever.
86 sess["takenOver"] = s.sessionMirrored(identityRoute)
87 }
88 if sessionPath != "" && store.IsSessionTranscriptName(filepath.Base(sessionPath)) {
89 sess["sessionName"] = strings.TrimSuffix(filepath.Base(sessionPath), ".jsonl")
90 sess["sessionPath"] = agent.CanonicalSessionPath(sessionPath)
91 }
92 addEffortStatus(sess, ctrl)
93 if canonical := agent.CanonicalSessionPath(sessionPath); canonical != "" && s.sessionMirrored(canonical) {
94 s.applyMirroredStatus(sess, canonical)
95 s.bindMu.Unlock()
96 s.maybeAutoReclaimMirrored(canonical)
97 writeJSON(w, sess)
98 return
99 }
100 if u := ctrl.LastUsage(); u != nil {
101 sess["lastUsage"] = u
102 }
103 sess["sessionCostQuote"] = s.bc.SessionCostQuoteFor(agent.CanonicalSessionPath(sessionPath))
104 if j := ctrl.Jobs(); len(j) > 0 {
105 sess["jobs"] = j
106 }
107 // Balance can perform provider IO and does not participate in session
108 // identity. Release the binding epoch before that optional slow request.
109 s.bindMu.Unlock()
110 if r.URL.Query().Get("runtime") != "1" && r.URL.Query().Get("lite") != "1" {
111 s.addBalanceStatus(r, sess, ctrl)
112 }
113 writeJSON(w, sess)
114 }
115
116 func sessionStatusFields(ctrl control.SessionAPI) map[string]any {
117 used, window := ctrl.ContextSnapshot()
118 hit, miss := ctrl.SessionCache()
119 state, rs := runtimeStateAndStatus(ctrl)
120 sess := map[string]any{
121 "runtimeState": state,
122 "label": ctrl.Label(),
123 "running": rs.Running,
124 "plan": ctrl.PlanMode(),
125 "autoApproveTools": ctrl.AutoApproveTools(),
126 "bypass": ctrl.AutoApproveTools(),
127 "toolApprovalMode": ctrl.ToolApprovalMode(),
128 "goal": ctrl.Goal(),
129 "goalStatus": ctrl.GoalStatus(),
130 "qualityFloor": ctrl.QualityFloor(),
131 "cwd": ctrl.SessionDir(),
132 "used": used,
133 "window": window,
134 "cacheHit": hit,
135 "cacheMiss": miss,
136 // Runtime reconciliation fields for desktop running-state watchdogs:
137 // the remote tab surface polls /status and maps these onto the same
138 // reconciliation the local tabs get from ListTabs.
139 "pendingPrompt": rs.PendingPrompt,
140 "backgroundJobs": rs.BackgroundJobs,
141 "cancelRequested": rs.CancelRequested,
142 "cancellable": rs.Cancellable,
143 }
144 if reader, ok := ctrl.(control.RuntimeStateReader); ok {
145 sess["goalView"] = reader.RuntimeStateSnapshot().Goal
146 }
147 if ctrl.Goal() != "" {
148 sess["goalRuntime"] = ctrl.GoalRuntime()
149 }
150 return sess
151 }
152
153 func addEffortStatus(sess map[string]any, ctrl control.SessionAPI) {
154 cfg, err := config.Load()
155 if err != nil {
156 return
157 }
158 entry, ok := cfg.ResolveModel(currentModelRef(ctrl))
159 if !ok {
160 return
161 }
162 capability := config.EffortCapabilityForEntry(entry)
163 levels := capability.Levels
164 if levels == nil {
165 levels = []string{}
166 }
167 sess["effort"] = map[string]any{
168 "supported": capability.Supported,
169 "current": config.EffortDisplay(entry),
170 "default": capability.Default,
171 "levels": levels,
172 }
173 }
174
175 // applyMirroredStatus states that a local runtime owns the session. These
176 // fields are the authoritative ownership signal: a slow subscriber can drop
177 // notices, the status poll cannot.
178 func (s *Server) applyMirroredStatus(sess map[string]any, canonical string) {
179 sess["running"] = false
180 sess["pendingPrompt"] = false
181 sess["takenOver"] = true
182 if m, ok := s.mirroredEntry(canonical); ok {
183 sess["reclaimRequested"] = m.reclaimRequested
184 }
185 }
186
187 func (s *Server) addBalanceStatus(r *http.Request, sess map[string]any, ctrl control.SessionAPI) {
188 b, err := ctrl.Balance(r.Context())
189 if err != nil {
190 slog.Warn("serve: balance fetch failed", "err", err)
191 return
192 }
193 if b == nil {
194 return
195 }
196 if cfg, loadErr := config.Load(); loadErr == nil && cfg.DisplayCurrencyPref() == "" {
197 // Runtime-only hint: a single wallet currency may select an existing
198 // valuation, but is never persisted as configuration or history.
199 s.bc.SetDisplayCurrency(b.PrimaryCurrency())
200 }
201 sess["balance"] = map[string]any{
202 "display": b.Display(),
203 "available": b.Available,
204 "infos": b.Infos,
205 }
206 }
207
207 lines GO