返回 DeepSeek-Reasonix
model_settings_detached.go
根目录 / internal / serve / model_settings_detached.go
1 package serve
2
3 import (
4 "context"
5 "fmt"
6 "time"
7
8 "reasonix/internal/control"
9 "reasonix/internal/sessioninbox"
10 )
11
12 // Background dispatch holds the same binding gate as foreground dispatch, then
13 // the owner's admission gate. The close watcher only takes the latter.
14 func (s *Server) beforeDetachedInboxDispatch(ctrl *control.Controller) (func(), error) {
15 s.bindMu.Lock()
16 s.detachedMu.Lock()
17 var owner *detachedSession
18 for _, d := range s.detached {
19 if d.ctrl == ctrl && !d.retiring {
20 owner = d
21 break
22 }
23 }
24 s.detachedMu.Unlock()
25 if owner == nil {
26 s.bindMu.Unlock()
27 return nil, control.ErrInboxRuntimeUnpublished
28 }
29 owner.admissionMu.Lock()
30 release := func() {
31 owner.admissionMu.Unlock()
32 s.bindMu.Unlock()
33 }
34 s.detachedMu.Lock()
35 valid := s.detached[owner.path] == owner && !owner.retiring && owner.ctrl == ctrl
36 s.detachedMu.Unlock()
37 if !valid {
38 release()
39 return nil, control.ErrInboxRuntimeUnpublished
40 }
41 if controllerHasActiveRuntimeWork(ctrl) {
42 release()
43 return nil, control.ErrTurnRunning
44 }
45 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
46 err := s.refreshModelSettingsOwnerLocked(ctx, modelSettingsRuntimeOwner{
47 current: func() control.SessionAPI { return owner.ctrl },
48 settings: &owner.modelSettings, offerID: &owner.modelSettingsOfferID,
49 apply: func(ctx context.Context, ref string) error { return s.rebuildDetachedModelSettings(ctx, owner, ref) },
50 })
51 cancel()
52 if err != nil {
53 release()
54 return nil, err
55 }
56 if owner.ctrl != ctrl {
57 replacement, _ := owner.ctrl.(*control.Controller)
58 release()
59 if replacement != nil {
60 replacement.NotifyInboxRuntimeReady()
61 }
62 return nil, control.ErrInboxRuntimeUnpublished
63 }
64 return release, nil
65 }
66
67 // Keep a background owner alive through the gap between completion and FIFO
68 // admission. Paused/blocked work remains durable and may close normally.
69 func (s *Server) detachedHasPendingWork(d *detachedSession) bool {
70 d.admissionMu.Lock()
71 defer d.admissionMu.Unlock()
72 if controllerHasActiveRuntimeWork(d.ctrl) {
73 return true
74 }
75 if inbox, ok := d.ctrl.(control.Inbox); ok {
76 snapshot := inbox.InboxSnapshot()
77 if !snapshot.Paused {
78 for _, item := range snapshot.Items {
79 if item.State == sessioninbox.StateQueued {
80 return true
81 }
82 }
83 }
84 }
85 s.detachedMu.Lock()
86 if s.detached[d.path] == d {
87 d.retiring = true
88 }
89 s.detachedMu.Unlock()
90 return false
91 }
92
93 func (s *Server) rebuildDetachedModelSettings(ctx context.Context, owner *detachedSession, ref string) error {
94 old, ok := owner.ctrl.(*control.Controller)
95 if !ok || controllerHasActiveRuntimeWork(old) {
96 return fmt.Errorf("background runtime cannot apply model settings yet")
97 }
98 if err := old.Snapshot(); err != nil {
99 return err
100 }
101 tag := newSessionTagSink(s.bc)
102 tag.PrimePath(old.SessionPath())
103 opts := owner.buildOptions
104 opts.Model, opts.ModelSettings, opts.Sink = ref, owner.modelSettings, tag
105 opts.SessionDir, opts.WorkspaceRoot = old.SessionDir(), old.WorkspaceRoot()
106 opts.BeforeInboxDispatch = s.beforeInboxDispatch
107 next, err := s.rebuildWithOptions(ctx, old, ref, opts, tag)
108 if err != nil {
109 return err
110 }
111 next.EnableInteractiveApproval()
112 next.SetOnSessionRecovered(s.sessionRecoveryHandler(next, owner.keeper))
113 if err := owner.keeper.BindControllerAuthority(next); err != nil {
114 s.closeTaggedController(next)
115 return err
116 }
117 if err := next.Snapshot(); err != nil {
118 _ = owner.keeper.BindControllerAuthority(old)
119 s.closeTaggedController(next)
120 return err
121 }
122 s.detachedMu.Lock()
123 owner.ctrl, owner.tag = next, tag
124 s.detachedMu.Unlock()
125 tag.Activate()
126 old.Close()
127 s.forgetSessionTag(old)
128 return nil
129 }
130
130 lines GO