返回 DeepSeek-Reasonix
sessionpath.go
根目录 / internal / control / sessionpath.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "log/slog"
7 "os"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/provider"
11 "reasonix/internal/session"
12 )
13
14 // EnsureSessionPath pins a fresh auto-save file for this controller when none is
15 // set yet and a session dir is configured — the "fresh session" branch every
16 // surface runs right after building a controller. It is a no-op once a resume or
17 // continue has already pinned a path (SessionPath() != ""), so callers can run a
18 // conditional Resume and then invoke this unconditionally. Centralises the
19 // per-surface copies of this logic (the CLI chat/serve fresh branches and the
20 // bot's former ensureControllerSessionPath).
21 func (c *Controller) EnsureSessionPath() {
22 c.ensureSessionPath(session.CreateOptions{})
23 }
24
25 // EnsureHeadlessRunSessionPath is EnsureSessionPath for a one-shot run: a
26 // fresh store records that kind so conversation lists can leave it out.
27 func (c *Controller) EnsureHeadlessRunSessionPath() {
28 c.ensureSessionPath(session.CreateOptions{Kind: session.SessionKindHeadlessRun})
29 }
30
31 func (c *Controller) ensureSessionPath(options session.CreateOptions) {
32 if _, ok := c.SessionRef(); ok {
33 return
34 }
35 if service, _, exclusive := c.v3Binding(); exclusive && service != nil {
36 if _, err := c.BindFreshSessionWithOptions(context.Background(), options); err != nil {
37 c.failTurnEventLedger(err)
38 }
39 return
40 }
41 if c.SessionPath() != "" || c.SessionDir() == "" {
42 return
43 }
44 c.SetFreshSessionPath(agent.NewSessionPath(c.SessionDir(), c.Label()))
45 }
46
47 // AdoptHistory makes a freshly built controller continue an existing
48 // conversation in path: it resumes the carried messages there when there are
49 // any, otherwise just points auto-save at path. An empty path with no messages
50 // is a no-op. This is the shared kernel of the model/effort switch across the
51 // CLI, the HTTP server, and ACP — each computes path its own way
52 // (ContinueSessionPath for the CLI/serve, the pinned transcript for ACP) and
53 // hands the carried history (Controller.History()) here. Keeping the
54 // Resume/SetSessionPath choice in one place avoids the orphaned-duplicate class
55 // of bug (#2807) recurring as each surface copied it.
56 func (c *Controller) AdoptHistory(msgs []provider.Message, path string) {
57 if c.sessionEngineEnabled() {
58 if _, ok := c.SessionRef(); ok {
59 if len(msgs) > 0 {
60 if err := c.replaceSessionEventProjection(context.Background(), "explicit-history-adopt", msgs); err != nil {
61 slog.Warn("controller: record adopted v3 history", "err", err)
62 c.failTurnEventLedger(err)
63 } else {
64 c.restoreExecutorFromSessionEvents()
65 }
66 } else {
67 c.restoreExecutorFromSessionEvents()
68 }
69 return
70 }
71 if path == "" {
72 // A hot rebuild may carry an in-memory conversation that never had
73 // persistent identity. Keep it as the candidate model/UI state; the
74 // first real input will create one v3 session and seed these exact
75 // messages. This is not a committed session until that admission.
76 if len(msgs) > 0 && c.executor != nil {
77 c.executor.SetSession(agent.NewSession("").CloneWithMessages(msgs))
78 }
79 return
80 }
81 if path != "" {
82 if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
83 if _, err := c.BindFreshSession(context.Background(), ""); err != nil {
84 c.failTurnEventLedger(err)
85 return
86 }
87 if len(msgs) > 0 {
88 if err := c.replaceSessionEventProjection(context.Background(), "fresh-history-adopt", msgs); err != nil {
89 c.failTurnEventLedger(err)
90 } else {
91 c.restoreExecutorFromSessionEvents()
92 }
93 }
94 return
95 }
96 loaded, err := agent.LoadSession(path)
97 if err == nil && len(msgs) > 0 {
98 loaded = loaded.CloneWithMessages(msgs)
99 }
100 if err == nil {
101 err = c.ResumeNativeSession(loaded, path)
102 }
103 if err != nil {
104 slog.Warn("controller: native legacy continue failed", "path", path, "err", err)
105 c.failTurnEventLedger(err)
106 }
107 }
108 return
109 }
110 if len(msgs) > 0 {
111 if path != "" {
112 if loaded, err := agent.LoadSession(path); err == nil && loaded != nil {
113 if resumed, ok := loaded.CloneWithMessagesIfCompatible(msgs); ok {
114 c.Resume(resumed, path)
115 return
116 }
117 }
118 }
119 c.Resume(agent.NewSession("").CloneWithMessages(msgs), path)
120 } else if path != "" {
121 // Even an empty transcript can carry session-scoped sidecars such as a
122 // running or blocked Goal. Resume a persisted empty session so controller
123 // rebuilds preserve that state; fall back to a plain binding for a fresh
124 // path that has not been saved yet.
125 if loaded, err := agent.LoadSession(path); err == nil && loaded != nil {
126 c.Resume(loaded, path)
127 return
128 }
129 c.SetSessionPath(path)
130 }
131 }
132
133 // AdoptNativeRebuiltContext retains the original format and selected head.
134 func (c *Controller) AdoptNativeRebuiltContext(old *Controller, msgs []provider.Message, path string) error {
135 if old == nil || old.executor == nil || old.executor.Session() == nil {
136 return errors.New("native rebuild requires the original session")
137 }
138 // Carry the loaded format and selected DAG head, including unsaved state.
139 // Reloading the path here would select the disk's default head instead.
140 if err := c.ResumeNativeSession(old.executor.Session().CloneWithMessages(msgs), path); err != nil {
141 return err
142 }
143 if c.UsesExclusiveSession() {
144 return c.AdoptRebuiltModelContext(msgs)
145 }
146 return nil
147 }
148
149 // AdoptRebuiltModelContext changes only the provider-visible context of a
150 // bound canonical session; display history stays on its original event stream.
151 func (c *Controller) AdoptRebuiltModelContext(msgs []provider.Message) error {
152 if !c.sessionEngineEnabled() {
153 return errors.New("model-context adoption requires an exclusive v3 session")
154 }
155 if len(msgs) == 0 {
156 if snapshot, ok := c.sessionEventSnapshot(); ok {
157 msgs = snapshot.Projection.ModelMessages
158 }
159 }
160 return c.replaceSessionModelContext(context.Background(), msgs, "agent-rebuild")
161 }
162
162 lines GO