返回 DeepSeek-Reasonix
multisession.go
根目录 / internal / serve / multisession.go
1 package serve
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "net/http"
9 "os"
10 "strings"
11 "sync"
12 "time"
13
14 "reasonix/internal/agent"
15 "reasonix/internal/boot"
16 "reasonix/internal/config"
17 "reasonix/internal/control"
18 "reasonix/internal/event"
19 "reasonix/internal/plugin"
20 )
21
22 // sessionTagSink stamps every event from one controller with that
23 // controller's current session path. This lets one Serve process keep several
24 // turns alive without sending a background session's frames to the foreground
25 // browser.
26 type sessionTagSink struct {
27 pendingRuntimeState *event.RuntimeStateSnapshot
28 bc *Broadcaster
29 mu sync.Mutex
30 path string
31 sessionID string
32 active bool
33 runtimeActive bool
34 pending []event.Event
35 }
36
37 func newSessionTagSink(bc *Broadcaster) *sessionTagSink {
38 return &sessionTagSink{bc: bc, runtimeActive: true}
39 }
40
41 // SessionTagSink is exported for the CLI, which builds Serve's initial
42 // controller before the Server exists.
43 type SessionTagSink = sessionTagSink
44
45 func NewSessionTagSink(bc *Broadcaster) *SessionTagSink {
46 return newSessionTagSink(bc)
47 }
48
49 func (s *sessionTagSink) SetPath(path string) {
50 s.SetIdentity(path, "")
51 }
52
53 func (s *sessionTagSink) SetIdentity(path, sessionID string) {
54 s.mu.Lock()
55 if s.path != "" && s.path != canonicalSessionPath(path) {
56 s.runtimeActive = false
57 }
58 s.path = canonicalSessionPath(path)
59 s.sessionID = strings.TrimSpace(sessionID)
60 s.activateLocked()
61 s.mu.Unlock()
62 }
63
64 // PrimePath assigns a replacement controller's route without publishing boot
65 // events. Activate is called only after the controller swap fully commits.
66 func (s *sessionTagSink) PrimePath(path string) {
67 s.mu.Lock()
68 s.path = canonicalSessionPath(path)
69 s.runtimeActive = s.bc.CurrentSession() == s.path
70 s.mu.Unlock()
71 }
72
73 // PrimeIdentity assigns a replacement controller's complete route without
74 // publishing its buffered boot events. Canonical v3 sessions have no legacy
75 // path, so PrimePath alone would drop the session ID from those events.
76 func (s *sessionTagSink) PrimeIdentity(path, sessionID string) {
77 s.mu.Lock()
78 s.path = canonicalSessionPath(path)
79 s.sessionID = strings.TrimSpace(sessionID)
80 s.runtimeActive = s.bc.CurrentSession() == s.path
81 s.mu.Unlock()
82 }
83
84 // BufferPath retags synchronous in-place Resume events but withholds them until
85 // Serve publishes the matching foreground route. Unlike PrimePath, it also
86 // pauses a sink that was already active for the previous session.
87 func (s *sessionTagSink) BufferPath(path string) {
88 s.mu.Lock()
89 s.path = canonicalSessionPath(path)
90 s.active = false
91 s.runtimeActive = false
92 s.mu.Unlock()
93 }
94
95 func canonicalSessionPath(path string) string {
96 if path != "" {
97 return agent.CanonicalSessionPath(path)
98 }
99 return ""
100 }
101
102 func (s *sessionTagSink) Activate() {
103 s.mu.Lock()
104 s.activateLocked()
105 s.mu.Unlock()
106 }
107
108 func (s *sessionTagSink) activateLocked() {
109 if s.active {
110 return
111 }
112 s.active = true
113 if s.runtimeActive && s.pendingRuntimeState != nil {
114 s.bc.publishRuntimeState(s.path, *s.pendingRuntimeState)
115 s.pendingRuntimeState = nil
116 }
117 for _, e := range s.pending {
118 if s.path != "" {
119 e.SessionPath = s.path
120 }
121 if s.sessionID != "" {
122 e.SessionID = s.sessionID
123 }
124 s.bc.Emit(e)
125 }
126 s.pending = nil
127 }
128
129 func (s *sessionTagSink) Path() string {
130 s.mu.Lock()
131 defer s.mu.Unlock()
132 return s.path
133 }
134
135 func (s *sessionTagSink) Emit(e event.Event) {
136 s.mu.Lock()
137 defer s.mu.Unlock()
138 if !s.active {
139 s.pending = append(s.pending, e)
140 return
141 }
142 if s.path != "" {
143 e.SessionPath = s.path
144 }
145 if s.sessionID != "" {
146 e.SessionID = s.sessionID
147 }
148 s.bc.Emit(e)
149 }
150
151 type detachedSession struct {
152 admissionMu sync.Mutex // new-run refresh and close-on-idle ownership
153 modelSettings *config.ModelRuntimeSettings
154 modelSettingsOfferID string
155 buildOptions boot.Options
156 path string
157 ctrl control.SessionAPI
158 keeper *control.SessionLeaseKeeper
159 tag *sessionTagSink
160 retiring bool // guarded by Server.detachedMu; blocks reattach during Close
161 force chan struct{}
162 reattach chan struct{}
163 done chan struct{}
164 }
165
166 // RegisterSessionTag associates a controller built outside Server with its
167 // tagging sink. In-place /new and /resume operations can then advance the tag.
168 func (s *Server) RegisterSessionTag(ctrl *control.Controller, tag *sessionTagSink) {
169 if ctrl == nil || tag == nil {
170 return
171 }
172 s.tagsMu.Lock()
173 if s.tags == nil {
174 s.tags = map[*control.Controller]*sessionTagSink{}
175 }
176 s.tags[ctrl] = tag
177 s.tagsMu.Unlock()
178 }
179
180 func (s *Server) tagFor(ctrl *control.Controller) *sessionTagSink {
181 if ctrl == nil {
182 return nil
183 }
184 s.tagsMu.Lock()
185 defer s.tagsMu.Unlock()
186 return s.tags[ctrl]
187 }
188
189 func (s *Server) forgetSessionTag(ctrl *control.Controller) {
190 if ctrl == nil {
191 return
192 }
193 s.tagsMu.Lock()
194 delete(s.tags, ctrl)
195 s.tagsMu.Unlock()
196 s.setControllerLeaseOwner(ctrl, nil)
197 }
198
199 func (s *Server) closeTaggedController(ctrl *control.Controller) {
200 if ctrl == nil {
201 return
202 }
203 ctrl.Close()
204 s.forgetSessionTag(ctrl)
205 }
206
207 func (s *Server) setControllerPath(ctrl *control.Controller, path string) {
208 if path != "" {
209 path = agent.CanonicalSessionPath(path)
210 }
211 if tag := s.tagFor(ctrl); tag != nil {
212 sessionID := ""
213 if ref, ok := ctrl.SessionRef(); ok {
214 sessionID = ref.SessionID
215 }
216 tag.SetIdentity(path, sessionID)
217 }
218 s.bc.SetCurrentSession(path)
219 }
220
221 // buildTagged creates a controller whose frames are session-tagged. The legacy
222 // two-argument test builder remains supported; tests that need to assert the
223 // complete boot contract can inject buildControllerWithOptions.
224 func (s *Server) buildTagged(ctx context.Context, ref string, inheritTemp bool) (*control.Controller, *sessionTagSink, error) {
225 tag := newSessionTagSink(s.bc)
226 opts := s.buildOptions
227 if s.managedModels != nil {
228 opts.ModelSettings = s.managedModels
229 }
230 opts.Model = ref
231 if cur := s.ctl(); cur != nil {
232 opts.EffortModel = currentModelRef(cur)
233 }
234 opts.BeforeInboxDispatch = s.beforeInboxDispatch
235 opts.Sink = tag
236 opts.BrowserExecutor = s.sessionBrowserExecutor(tag)
237 if opts.Stderr == nil {
238 opts.Stderr = os.Stderr
239 }
240 opts.StatsSource = "serve"
241 opts.MCPHostProfile = plugin.HostProfileInteractive
242 if cur, ok := s.ctl().(*control.Controller); ok && cur != nil {
243 opts.SessionDir = cur.SessionDir()
244 opts.WorkspaceRoot = cur.WorkspaceRoot()
245 if inheritTemp {
246 opts.SessionTemp = cur.SessionTemp()
247 // Model/effort switches rebuild the Agent, not the logical session:
248 // without the bound runtime the rebuilt controller's first submit
249 // allocates a new ID while the desktop fences the old one (HTTP 409).
250 if service, runtime, bound := cur.SessionBinding(); bound {
251 opts.SessionService = service
252 opts.SessionRuntime = runtime
253 opts.SessionHostID = runtime.Ref().HostID
254 }
255 }
256 }
257
258 var (
259 ctrl *control.Controller
260 err error
261 )
262 switch {
263 case s.buildControllerWithOptions != nil:
264 ctrl, err = s.buildControllerWithOptions(ctx, ref, opts)
265 case s.buildController != nil:
266 ctrl, err = s.buildController(ctx, ref)
267 default:
268 ctrl, err = boot.Build(ctx, opts)
269 }
270 if err != nil {
271 return nil, nil, err
272 }
273 s.RegisterSessionTag(ctrl, tag)
274 slog.Info("serve: controller built", "model", ref, "sessionDir", opts.SessionDir)
275 return ctrl, tag, nil
276 }
277
278 func (s *Server) detachedBusy(path string) bool {
279 path = agent.CanonicalSessionPath(path)
280 s.detachedMu.Lock()
281 defer s.detachedMu.Unlock()
282 _, ok := s.detached[path]
283 return ok
284 }
285
286 // takeDetached transfers ownership from the close-on-idle watcher back to the
287 // request goroutine. Waiting for done is essential: without the acknowledgement
288 // the watcher can close an idle controller just after it is re-attached.
289 func (s *Server) takeDetached(path string) *detachedSession {
290 path = agent.CanonicalSessionPath(path)
291 s.detachedMu.Lock()
292 d := s.detached[path]
293 if d != nil && !d.retiring {
294 delete(s.detached, path)
295 } else {
296 d = nil
297 }
298 s.detachedMu.Unlock()
299 if d == nil {
300 return nil
301 }
302 close(d.reattach)
303 <-d.done
304 return d
305 }
306
307 func (s *Server) registerDetached(ctrl control.SessionAPI, keeper *control.SessionLeaseKeeper, tag *sessionTagSink) (*detachedSession, error) {
308 if ctrl == nil {
309 return nil, fmt.Errorf("cannot detach a nil controller")
310 }
311 if tag == nil {
312 if concrete, ok := ctrl.(*control.Controller); ok {
313 tag = s.tagFor(concrete)
314 }
315 }
316 if tag == nil {
317 return nil, errSessionTagUnavailable
318 }
319 if keeper != nil {
320 if concrete, ok := ctrl.(*control.Controller); ok {
321 concrete.SetOnSessionRecovered(s.sessionRecoveryHandler(concrete, keeper))
322 }
323 }
324 if registerDetachedHookForTest != nil {
325 registerDetachedHookForTest()
326 }
327 d := &detachedSession{
328 ctrl: ctrl, keeper: keeper, tag: tag,
329 modelSettings: s.managedModels, modelSettingsOfferID: s.modelSettingsOfferID, buildOptions: s.buildOptions,
330 force: make(chan struct{}), reattach: make(chan struct{}), done: make(chan struct{}),
331 }
332 s.detachedMu.Lock()
333 path := agent.CanonicalSessionPath(ctrl.SessionPath())
334 if path == "" {
335 s.detachedMu.Unlock()
336 return nil, fmt.Errorf("cannot detach a session without a path")
337 }
338 d.path = path
339 if s.detached == nil {
340 s.detached = map[string]*detachedSession{}
341 }
342 if _, exists := s.detached[path]; exists {
343 s.detachedMu.Unlock()
344 return nil, fmt.Errorf("session is already running in the background")
345 }
346 s.detached[path] = d
347 s.detachedMu.Unlock()
348 slog.Info("serve: session detached", "session", path, "running", controllerHasActiveRuntimeWork(ctrl))
349 go s.watchDetached(d)
350 if concrete, ok := ctrl.(*control.Controller); ok {
351 concrete.NotifyInboxRuntimeReady()
352 }
353 return d, nil
354 }
355
356 func (s *Server) watchDetached(d *detachedSession) {
357 interval := 200 * time.Millisecond
358 forced := false
359 for s.detachedHasPendingWork(d) && !forced {
360 timer := time.NewTimer(interval)
361 select {
362 case <-d.reattach:
363 if !timer.Stop() {
364 <-timer.C
365 }
366 close(d.done)
367 return
368 case <-d.force:
369 if !timer.Stop() {
370 <-timer.C
371 }
372 forced = true
373 case <-timer.C:
374 if interval < 2*time.Second {
375 interval *= 2
376 }
377 }
378 }
379
380 // Claim close ownership only while the registry still points at d. Keep the
381 // retiring entry visible until Close and lease release finish so deletion
382 // cannot race final controller writes. takeDetached refuses retiring entries.
383 d.admissionMu.Lock()
384 s.detachedMu.Lock()
385 owns := s.detached[d.path] == d
386 if owns {
387 d.retiring = true
388 }
389 s.detachedMu.Unlock()
390 d.admissionMu.Unlock()
391 if !owns {
392 close(d.done)
393 return
394 }
395 d.ctrl.Close()
396 if d.keeper != nil {
397 d.keeper.Release()
398 }
399 if concrete, ok := d.ctrl.(*control.Controller); ok {
400 s.forgetSessionTag(concrete)
401 }
402 s.detachedMu.Lock()
403 closedPath := d.path
404 if s.detached[d.path] == d {
405 delete(s.detached, d.path)
406 }
407 s.detachedMu.Unlock()
408 slog.Info("serve: background session closed", "session", closedPath, "forced", forced)
409 close(d.done)
410 }
411
412 func (s *Server) WaitForDetachedIdle() {
413 s.detachedMu.Lock()
414 detached := make([]*detachedSession, 0, len(s.detached))
415 for _, d := range s.detached {
416 detached = append(detached, d)
417 }
418 s.detachedMu.Unlock()
419 for _, d := range detached {
420 <-d.done
421 }
422 }
423
424 func (s *Server) CloseBackground() {
425 s.detachedMu.Lock()
426 detached := make([]*detachedSession, 0, len(s.detached))
427 for _, d := range s.detached {
428 detached = append(detached, d)
429 }
430 s.detachedMu.Unlock()
431 for _, d := range detached {
432 select {
433 case <-d.force:
434 default:
435 close(d.force)
436 }
437 }
438 for _, d := range detached {
439 <-d.done
440 }
441 }
442
443 // Close stops every controller the server owns, including a foreground
444 // replacement created after the CLI's original controller was constructed.
445 func (s *Server) Close() {
446 s.CloseBackground()
447 cur := s.ctl()
448 cur.Close()
449 if concrete, ok := cur.(*control.Controller); ok {
450 s.forgetSessionTag(concrete)
451 }
452 }
453
454 // busyDetach publishes a fresh controller before demoting a busy controller.
455 // Every failure before publication restores the original lease ownership.
456 func (s *Server) busyDetach(ctx context.Context, cur *control.Controller, targetPath string, loadTarget func(*control.Controller) error) error {
457 if s.tagFor(cur) == nil {
458 return errSessionTagUnavailable
459 }
460 newCtrl, tag, err := s.buildTagged(ctx, currentModelRef(cur), false)
461 if err != nil {
462 return err
463 }
464 if targetPath == "" {
465 newCtrl.EnsureSessionPath()
466 targetPath = newCtrl.SessionPath()
467 }
468 targetPath = agent.CanonicalSessionPath(targetPath)
469 if targetPath == "" {
470 s.closeTaggedController(newCtrl)
471 return fmt.Errorf("replacement session has no path")
472 }
473
474 demoted, err := s.leases.RebindDetaching(targetPath)
475 if err != nil {
476 s.closeTaggedController(newCtrl)
477 return err
478 }
479 if loadTarget != nil {
480 if err := loadTarget(newCtrl); err != nil {
481 s.closeTaggedController(newCtrl)
482 s.rollbackDetach(demoted, cur)
483 return err
484 }
485 }
486 // PrimeIdentity keeps the session id alive across the swap: exclusive
487 // sessions have no path, and PrimePath alone would strip the id from every
488 // frame the replacement controller emits.
489 if ref, ok := newCtrl.SessionRef(); ok {
490 tag.PrimeIdentity(targetPath, ref.SessionID)
491 } else {
492 tag.PrimePath(targetPath)
493 }
494 newCtrl.EnableInteractiveApproval()
495 newCtrl.SetOnSessionRecovered(s.sessionRecoveryHandler(newCtrl, s.leases))
496 if s.leases != nil {
497 if err := s.leases.BindControllerAuthority(newCtrl); err != nil {
498 s.closeTaggedController(newCtrl)
499 s.rollbackDetach(demoted, cur)
500 return err
501 }
502 }
503
504 if !s.publishControllerSwap(cur, newCtrl, targetPath) {
505 s.closeTaggedController(newCtrl)
506 s.rollbackDetach(demoted, cur)
507 return errReplacedDuringBind
508 }
509
510 if _, err := s.registerDetached(cur, demoted, nil); err != nil {
511 // bindMu prevents another foreground swap here. Roll publication back so
512 // a registry failure cannot strand a running controller.
513 _ = s.publishControllerSwap(newCtrl, cur, cur.SessionPath())
514 s.closeTaggedController(newCtrl)
515 s.rollbackDetach(demoted, cur)
516 return err
517 }
518 tag.Activate()
519 s.bc.ResetSessionPath(targetPath)
520 return nil
521 }
522
523 func (s *Server) announceSessionChanged(path string, reset bool) {
524 e := event.Event{Kind: event.SessionChanged, SessionPath: path, SessionReset: reset}
525 if identity, ok := s.ctl().(control.IdentityLifecycle); ok {
526 if ref, bound := identity.SessionRef(); bound {
527 e.SessionID = ref.SessionID
528 }
529 }
530 s.bc.Emit(e)
531 if ctrl, ok := s.ctl().(*control.Controller); ok {
532 if tag := s.tagFor(ctrl); tag != nil {
533 tag.ActivateRuntime()
534 }
535 }
536 }
537
538 // The routing barrier precedes the new instance's runtime projection. Content
539 // boot notices retain their historical order relative to session_changed.
540 func (s *sessionTagSink) ActivateRuntime() {
541 s.mu.Lock()
542 defer s.mu.Unlock()
543 s.runtimeActive = true
544 if s.active && s.pendingRuntimeState != nil {
545 s.bc.publishRuntimeState(s.path, *s.pendingRuntimeState)
546 s.pendingRuntimeState = nil
547 }
548 }
549
550 var errReplacedDuringBind = &replacedDuringBindError{}
551 var errSessionTagUnavailable = errors.New("multi-session switching requires a session-tagged Serve controller")
552
553 type replacedDuringBindError struct{}
554
555 func (*replacedDuringBindError) Error() string { return "session changed during switch" }
556
557 func (s *Server) rollbackDetach(demoted *control.SessionLeaseKeeper, ctrl *control.Controller) {
558 if demoted != nil && s.leases != nil {
559 s.leases.Adopt(demoted)
560 }
561 if ctrl != nil {
562 ctrl.SetOnSessionRecovered(s.sessionRecoveryHandler(ctrl, s.leases))
563 }
564 }
565
566 func (s *Server) resumeActiveSession(w http.ResponseWriter, r *http.Request, cur control.SessionAPI, realPath string) bool {
567 if agent.CanonicalSessionPath(cur.SessionPath()) == agent.CanonicalSessionPath(realPath) {
568 s.bc.SetCurrentSession(realPath)
569 s.announceSessionChanged(realPath, false)
570 w.WriteHeader(http.StatusNoContent)
571 return true
572 }
573 if detached := s.takeDetached(realPath); detached != nil {
574 if err := s.reattachDetached(cur, detached); err != nil {
575 s.renderBindError(w, err)
576 return true
577 }
578 s.announceSessionChanged(realPath, false)
579 w.WriteHeader(http.StatusNoContent)
580 s.replayPendingPromptsBroadcast()
581 return true
582 }
583 if s.detachedBusy(realPath) {
584 http.Error(w, "session is finishing background teardown; retry shortly", http.StatusConflict)
585 return true
586 }
587 if !controllerHasActiveRuntimeWork(cur) {
588 return false
589 }
590 curCtrl, ok := cur.(*control.Controller)
591 if !ok {
592 http.Error(w, "cannot switch session while active work or background jobs are running", http.StatusConflict)
593 return true
594 }
595 err := s.busyDetach(r.Context(), curCtrl, realPath, func(next *control.Controller) error {
596 loaded, err := agent.LoadSession(realPath)
597 if err == nil {
598 next.Resume(loaded, realPath)
599 }
600 return err
601 })
602 if err != nil {
603 s.renderBindError(w, err)
604 return true
605 }
606 s.announceSessionChanged(realPath, false)
607 w.WriteHeader(http.StatusNoContent)
608 s.replayPendingPromptsBroadcast()
609 return true
610 }
611
612 // reattachDetached promotes a controller owned by the background registry.
613 // bindMu is held, so publication and lease ownership move as one transaction.
614 func (s *Server) reattachDetached(cur control.SessionAPI, detached *detachedSession) error {
615 curCtrl, _ := cur.(*control.Controller)
616 demoted := s.leases.Split()
617 s.leases.Adopt(detached.keeper)
618 detached.keeper = nil
619 if detached.tag != nil {
620 detached.tag.SetPath(detached.ctrl.SessionPath())
621 }
622 if concrete, ok := detached.ctrl.(*control.Controller); ok {
623 concrete.SetOnSessionRecovered(s.sessionRecoveryHandler(concrete, s.leases))
624 }
625 if !s.publishControllerSwap(cur, detached.ctrl, detached.ctrl.SessionPath()) {
626 detached.keeper = s.leases.Split()
627 s.leases.Adopt(demoted)
628 if curCtrl != nil {
629 curCtrl.SetOnSessionRecovered(s.sessionRecoveryHandler(curCtrl, s.leases))
630 }
631 _, _ = s.registerDetached(detached.ctrl, detached.keeper, detached.tag)
632 return errReplacedDuringBind
633 }
634 if controllerHasActiveRuntimeWork(cur) {
635 if curCtrl == nil {
636 s.restoreReattach(cur, detached, demoted)
637 return fmt.Errorf("cannot switch session while active work or background jobs are running")
638 }
639 if _, err := s.registerDetached(curCtrl, demoted, nil); err != nil {
640 s.restoreReattach(cur, detached, demoted)
641 return err
642 }
643 } else {
644 if err := cur.Snapshot(); err != nil {
645 slog.Warn("serve: snapshot before background reattach", "err", err)
646 }
647 cur.Close()
648 if curCtrl != nil {
649 s.forgetSessionTag(curCtrl)
650 }
651 if demoted != nil {
652 demoted.Release()
653 }
654 }
655 s.managedModels, s.modelSettingsOfferID = detached.modelSettings, detached.modelSettingsOfferID
656 s.buildOptions = detached.buildOptions
657 slog.Info("serve: background session re-attached", "session", detached.path, "running", controllerHasActiveRuntimeWork(detached.ctrl))
658 return nil
659 }
660
661 func (s *Server) restoreReattach(cur control.SessionAPI, detached *detachedSession, demoted *control.SessionLeaseKeeper) {
662 _ = s.publishControllerSwap(detached.ctrl, cur, cur.SessionPath())
663 detached.keeper = s.leases.Split()
664 s.leases.Adopt(demoted)
665 if concrete, ok := cur.(*control.Controller); ok {
666 concrete.SetOnSessionRecovered(s.sessionRecoveryHandler(concrete, s.leases))
667 }
668 _, _ = s.registerDetached(detached.ctrl, detached.keeper, detached.tag)
669 }
670
671 // publishControllerSwap makes the command target and current-only SSE route
672 // visible as one generation. Readers cannot observe next while the broadcaster
673 // still filters against expect's session.
674 func (s *Server) publishControllerSwap(expect, next control.SessionAPI, path string) bool {
675 s.mu.Lock()
676 defer s.mu.Unlock()
677 if s.ctrl != expect {
678 return false
679 }
680 if err := control.ActivateSessionAPIReplacement(expect, next); err != nil {
681 slog.Warn("serve: activate controller replacement", "err", err)
682 return false
683 }
684 s.ctrl = next
685 s.bc.SetCurrentSession(path)
686 if ctrl, ok := next.(*control.Controller); ok {
687 ctrl.SetBeforeInboxDispatch(s.beforeInboxDispatch)
688 ctrl.NotifyInboxRuntimeReady()
689 }
690 return true
691 }
692
693 func (s *Server) replayPendingPromptsBroadcast() {
694 cur := s.ctl()
695 path := cur.SessionPath()
696 cur.ReplayPendingPromptsWith(func() event.Sink {
697 return event.FuncSink(func(e event.Event) {
698 e.SessionPath = path
699 s.bc.Emit(e)
700 })
701 })
702 }
703
704 func (s *Server) renderBindError(w http.ResponseWriter, err error) {
705 switch {
706 case errors.Is(err, agent.ErrSessionLeaseHeld):
707 http.Error(w, sessionInUseError(err), http.StatusConflict)
708 case errors.Is(err, errReplacedDuringBind), errors.Is(err, errSessionTagUnavailable):
709 http.Error(w, err.Error(), http.StatusConflict)
710 default:
711 http.Error(w, "switch session: "+err.Error(), http.StatusInternalServerError)
712 }
713 }
714
715 // retireDetachedForProviderHeal is called with bindMu held. It makes every
716 // detached controller unreattachable and waits for its provider generation to
717 // close before credential reload is acknowledged.
718 func (s *Server) retireDetachedForProviderHeal() {
719 s.detachedMu.Lock()
720 detached := make([]*detachedSession, 0, len(s.detached))
721 for _, d := range s.detached {
722 detached = append(detached, d)
723 select {
724 case <-d.force:
725 default:
726 close(d.force)
727 }
728 }
729 s.detachedMu.Unlock()
730 for _, d := range detached {
731 slog.Info("serve: provider heal retires background session", "session", d.path)
732 <-d.done
733 }
734 }
735
735 lines GO