返回 DeepSeek-Reasonix
session_transition.go
根目录 / internal / control / session_transition.go
1 package control
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/guardian"
9 )
10
11 // SessionTransitionInfo describes an intentional controller path change. The
12 // candidate stays private; its owner binds it through BindWriteAuthority before
13 // the controller publishes it as current.
14 type SessionTransitionInfo struct {
15 OriginalPath string
16 TargetPath string
17 Reason string
18
19 session *agent.Session
20 commit *sessionTransitionCommit
21 }
22
23 type sessionTransitionCommit struct {
24 controller *Controller
25 targetPath string
26 session *agent.Session
27 hooks []func()
28 }
29
30 // BindWriteAuthority binds the transition candidate to lease.
31 func (i SessionTransitionInfo) BindWriteAuthority(lease *agent.SessionLease) error {
32 if i.session == nil {
33 return fmt.Errorf("session transition candidate is unavailable")
34 }
35 if lease == nil {
36 i.session.RequireWriteAuthority()
37 i.session.ClearWriteAuthority()
38 return agent.ErrSessionWriteAuthorityMissing
39 }
40 if lease.Path() != agent.CanonicalSessionPath(i.TargetPath) {
41 return fmt.Errorf("session transition lease does not cover target")
42 }
43 return lease.Writer().Bind(i.session, agent.NextSessionWriteGeneration())
44 }
45
46 // OnCommit defers publication work until the Controller has committed both its
47 // session path and executor Session to the transition target.
48 func (i SessionTransitionInfo) OnCommit(fn func()) {
49 if i.commit == nil || fn == nil {
50 return
51 }
52 i.commit.hooks = append(i.commit.hooks, fn)
53 }
54
55 func (c *sessionTransitionCommit) publish() {
56 if c == nil {
57 return
58 }
59 c.controller.mu.Lock()
60 c.controller.sessionPath = c.targetPath
61 c.controller.guardianPath = guardian.PathFor(c.targetPath)
62 c.controller.mu.Unlock()
63 c.controller.setActiveJobSession(c.targetPath)
64 if c.controller.executor != nil {
65 c.controller.executor.SetSession(c.session)
66 }
67 for _, fn := range c.hooks {
68 fn()
69 }
70 c.hooks = nil
71 }
72
73 // SetOnSessionTransition installs the owner handoff used before a path change.
74 func (c *Controller) SetOnSessionTransition(fn func(SessionTransitionInfo) error) {
75 if c == nil {
76 return
77 }
78 c.mu.Lock()
79 c.onSessionTransition = fn
80 c.mu.Unlock()
81 // Once a host delegates path transitions to a lease owner, unpublished
82 // replacements must remain fenced even if that callback is later detached
83 // during shutdown. Downgrading to permissive event writes would let a stale
84 // controller mutate the shared v3 projection.
85 if fn != nil {
86 c.managedSessionEvents.Store(true)
87 }
88 }
89
90 func (c *Controller) sessionTransitionHandler() func(SessionTransitionInfo) error {
91 c.mu.Lock()
92 defer c.mu.Unlock()
93 return c.onSessionTransition
94 }
95
96 func (c *Controller) prepareSessionTransition(targetPath, reason string, candidate *agent.Session) (*sessionTransitionCommit, error) {
97 targetPath = strings.TrimSpace(targetPath)
98 if candidate == nil {
99 return nil, fmt.Errorf("session transition target is unavailable")
100 }
101 commit := &sessionTransitionCommit{controller: c, targetPath: targetPath, session: candidate}
102 // In-memory controllers intentionally rotate Sessions without persistence.
103 // They have no lease or routable path to transfer, but still need the same
104 // atomic executor swap used by persisted controllers.
105 if targetPath == "" {
106 return commit, nil
107 }
108 handler := c.sessionTransitionHandler()
109 if handler == nil {
110 // Embedded/test controllers that never required a writer retain their
111 // permissive behavior. A writer-bound controller must fail closed.
112 if current := c.executor.Session(); current != nil && current.WriteAuthorityRequired() {
113 candidate.RequireWriteAuthority()
114 return nil, agent.ErrSessionWriteAuthorityMissing
115 }
116 return commit, nil
117 }
118 info := SessionTransitionInfo{
119 OriginalPath: c.SessionPath(),
120 TargetPath: targetPath,
121 Reason: reason,
122 session: candidate,
123 commit: commit,
124 }
125 if err := handler(info); err != nil {
126 return nil, err
127 }
128 auth := candidate.WriteAuthority()
129 if candidate.WriteAuthorityRequired() && (auth == nil || !auth.Covers(targetPath)) {
130 return nil, agent.ErrSessionWriteAuthorityMissing
131 }
132 return commit, nil
133 }
134
134 lines GO