| 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 |