返回 DeepSeek-Reasonix
fork_targets.go
根目录 / internal / control / fork_targets.go
1 package control
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/session"
9 )
10
11 // ForkTargets reports the fork state of this controller's current session: the
12 // turns a client may cut at, each with the reason it is or is not forkable yet.
13 // It reads committed turn records only, so it answers while a turn is running
14 // and never takes the rotation gate that ForkSession and ForkNamed hold.
15 func (c *Controller) ForkTargets() (session.ForkTargetSet, error) {
16 service, runtime, exclusive := c.v3Binding()
17 if !exclusive {
18 // The checkpoint engine records message counts rather than turn records, so
19 // no boundary can be proven from it: an empty, unverifiable set is the honest
20 // answer. A controller with no executor has no session and stays an error.
21 if c == nil || c.executor == nil {
22 return session.ForkTargetSet{}, fmt.Errorf("checkpoints unavailable")
23 }
24 return session.ForkTargetSet{Targets: []session.ForkTarget{}, Verifiable: false}, nil
25 }
26 if service == nil || runtime == nil {
27 return session.ForkTargetSet{}, session.ErrSessionNotRunning
28 }
29 // v3Binding copied the exact service and runtime out from under its own read
30 // lock, and SessionRef is immutable, so the source identity is settled before
31 // the read below: no controller lock is held across it.
32 return service.ForkTargetSetFor(context.Background(), runtime.Ref())
33 }
34
35 // CreateForkSession creates an independent child session from the completed turn
36 // named by the request, titled name when it is non-empty, and
37 // returns the child's id. Unlike ForkSession it takes no rotation gate: it runs
38 // while a turn is in flight, leaves that turn and this controller's own session
39 // untouched, and publishes no runtime — whichever surface shows the child opens
40 // it later.
41 func (c *Controller) CreateForkSession(request session.ForkRequest, name string) (childSessionID string, err error) {
42 service, parent, err := c.forkSourceRuntime(request.Source)
43 if err != nil {
44 return "", err
45 }
46 // The exact runtime captured above is authoritative, but keep its ref in the
47 // request so Service.CreateFork also validates the same immutable source.
48 request.Source = parent.Ref()
49 result, err := service.CreateFork(context.Background(), request)
50 if err != nil {
51 return "", err
52 }
53 // Service.CreateFork publishes the child's durable prefix and returns its
54 // identity without opening a runtime, so this call owns no child handle to
55 // release; Service.Fork, used by the switching path, opens and closes one.
56 if title := strings.TrimSpace(name); title != "" {
57 // The child has no runtime here, so the title goes through the service's
58 // cold writer: the same session/title event the switching fork appends,
59 // flushed under a writer lease this call takes and releases itself.
60 if err := service.SetTitle(context.Background(), result.Child, title); err != nil {
61 return "", err
62 }
63 }
64 return result.Child.SessionID, nil
65 }
66
67 // forkSourceRuntime resolves the v3 service and the exact live runtime of this
68 // controller's session. It mirrors branch_ops: sessionEngineEnabled selects the
69 // v3 engine, and a controller that is not on it — or that is on it without an
70 // active runtime — has no source identity to fork from.
71 func (c *Controller) forkSourceRuntime(expected session.SessionRef) (*session.Service, *session.Runtime, error) {
72 if c == nil {
73 return nil, nil, session.ErrSessionNotRunning
74 }
75 service, runtime, exclusive := c.v3Binding()
76 if !exclusive || service == nil || runtime == nil {
77 return nil, nil, session.ErrSessionNotRunning
78 }
79 if runtime.Ref() != expected {
80 return nil, nil, &session.ForkUnavailableError{TurnID: "", Reason: session.ForkStaleSource}
81 }
82 return service, runtime, nil
83 }
84
84 lines GO