返回 DeepSeek-Reasonix
session.go
根目录 / internal / store / session.go
1 // Package store is the single authority for reasonix's on-disk persistence
2 // layout. Nothing else should construct a persistence path by hand.
3 //
4 // This first slice owns the session-artifact sidecars — the files and
5 // directories that live beside a session's .jsonl (branch metadata, goal state,
6 // checkpoints, background-job artifacts, the cleanup-pending marker). They were
7 // previously derived independently in internal/agent, internal/jobs,
8 // internal/control and internal/acp, each re-spelling the suffix convention; a
9 // layout change meant hunting across packages. Centralizing them here makes
10 // store the one place that knows where a session's artifacts go.
11 //
12 // store is a leaf: it imports only the standard library, so any package may
13 // depend on it without risking an import cycle. Root/directory resolution (the
14 // ~/.reasonix tree) and the desktop root unification land in later slices.
15 package store
16
17 import "strings"
18
19 // IsSessionTranscriptName reports whether name is a primary session transcript
20 // file. Append-only event logs and guardian sidecars also end in .jsonl, so
21 // callers that discover sessions by directory scan must use this helper instead
22 // of filepath.Ext.
23 func IsSessionTranscriptName(name string) bool {
24 name = strings.TrimSpace(name)
25 return strings.HasSuffix(name, ".jsonl") &&
26 !strings.HasSuffix(name, ".events.jsonl") &&
27 !strings.HasSuffix(name, ".turns.jsonl") &&
28 !strings.HasSuffix(name, ".conflicts.jsonl") &&
29 !strings.HasSuffix(name, ".guardian.jsonl")
30 }
31
32 // SessionRecoveryState is the persisted Auto-mode recovery checkpoint state
33 // (<id>.recovery.json). It is a regular session-owned sidecar, not a transcript.
34 func SessionRecoveryState(sessionPath string) string {
35 sessionPath = strings.TrimSpace(sessionPath)
36 if sessionPath == "" {
37 return ""
38 }
39 return sessionStem(sessionPath) + ".recovery.json"
40 }
41
42 // SessionTranscriptProjection stores display-only records and their exact
43 // committed event coverage; it never supplies provider-visible messages.
44 func SessionTranscriptProjection(sessionPath string) string {
45 if strings.TrimSpace(sessionPath) == "" {
46 return ""
47 }
48 return sessionStem(sessionPath) + ".transcript-projection.json"
49 }
50
51 // SessionContext is the context-projection / compaction-state sidecar
52 // (<id>.context.json). It holds the model-visible projection and cache
53 // telemetry; transcript authority remains with the native event log once one
54 // exists, with the primary .jsonl retained as its compatibility checkpoint.
55 func SessionContext(sessionPath string) string {
56 sessionPath = strings.TrimSpace(sessionPath)
57 if sessionPath == "" {
58 return ""
59 }
60 return sessionStem(sessionPath) + ".context.json"
61 }
62
63 // SessionPinnedContext is the optional desktop pinned-workspace-context
64 // sidecar (<id>.pinned-context.json). Older versions ignore it while keeping
65 // the primary transcript fully readable.
66 func SessionPinnedContext(sessionPath string) string {
67 sessionPath = strings.TrimSpace(sessionPath)
68 if sessionPath == "" {
69 return ""
70 }
71 return sessionStem(sessionPath) + ".pinned-context.json"
72 }
73
74 // sessionStem strips the .jsonl suffix so a sidecar sits beside the session as
75 // <id>.<kind> rather than <id>.jsonl.<kind>.
76 func sessionStem(sessionPath string) string {
77 return strings.TrimSuffix(sessionPath, ".jsonl")
78 }
79
80 // SessionMeta is the branch-metadata sidecar. Unlike the other sidecars it
81 // appends to the full session path (historical layout), so session.jsonl yields
82 // session.jsonl.meta.
83 func SessionMeta(sessionPath string) string {
84 if sessionPath == "" {
85 return ""
86 }
87 return sessionPath + ".meta"
88 }
89
90 // SessionGoalState is the persisted active-goal sidecar (<id>.goal-state.json).
91 func SessionGoalState(sessionPath string) string {
92 if sessionPath == "" {
93 return ""
94 }
95 return sessionStem(sessionPath) + ".goal-state.json"
96 }
97
98 // SessionEventLog is the append-only transcript event log (<id>.events.jsonl).
99 func SessionEventLog(sessionPath string) string {
100 if sessionPath == "" {
101 return ""
102 }
103 return sessionStem(sessionPath) + ".events.jsonl"
104 }
105
106 // SessionEventLogDamaged is the salvage sidecar for event-log bytes that tail
107 // repair would otherwise discard (<id>.events.jsonl.damaged). It must NOT end
108 // in .jsonl: older binaries scanning a shared session directory classify any
109 // non-excluded .jsonl file as a primary transcript and would resurrect the
110 // damaged bytes as a phantom session.
111 func SessionEventLogDamaged(sessionPath string) string {
112 if sessionPath == "" {
113 return ""
114 }
115 return SessionEventLog(sessionPath) + ".damaged"
116 }
117
118 // SessionEventLogRotating marks a schema-2 log whose rotation is between
119 // reading the old file and publishing the new one; unlocked appenders wait
120 // for it to clear before trusting that their bytes reached the current log.
121 func SessionEventLogRotating(sessionPath string) string {
122 if sessionPath == "" {
123 return ""
124 }
125 return SessionEventLog(sessionPath) + ".rotating"
126 }
127
128 // SessionTurnEventLog is the append-only local runtime lifecycle ledger
129 // (<id>.turns.jsonl). It is independent from the provider transcript so old
130 // readers can continue to consume the primary session unchanged.
131 func SessionTurnEventLog(sessionPath string) string {
132 if sessionPath == "" {
133 return ""
134 }
135 return sessionStem(sessionPath) + ".turns.jsonl"
136 }
137
138 // SessionTurnEventLogDamaged preserves a corrupt/torn ledger tail before the
139 // valid prefix is truncated back into service.
140 func SessionTurnEventLogDamaged(sessionPath string) string {
141 if sessionPath == "" {
142 return ""
143 }
144 return SessionTurnEventLog(sessionPath) + ".damaged"
145 }
146
147 // SessionEventIndex is the listing/checkpoint index for the event log
148 // (<id>.event-index.json). It contains derived offsets and digests, not the
149 // transcript body.
150 func SessionEventIndex(sessionPath string) string {
151 if sessionPath == "" {
152 return ""
153 }
154 return sessionStem(sessionPath) + ".event-index.json"
155 }
156
157 // SessionDisplayIndex is the paging sidecar for the transcript
158 // (<id>.display-index.json). It contains per-message byte offsets, roles, and
159 // turn boundaries derived from the transcript, never message bodies, so a
160 // reader can page a huge history without parsing whole session files.
161 func SessionDisplayIndex(sessionPath string) string {
162 if sessionPath == "" {
163 return ""
164 }
165 return sessionStem(sessionPath) + ".display-index.json"
166 }
167
168 // SessionConflictLog is the append-only diagnostic log for snapshot conflict
169 // recoveries (<id>.conflicts.jsonl). It contains revision counters and branch
170 // ids, not transcript content.
171 func SessionConflictLog(sessionPath string) string {
172 if sessionPath == "" {
173 return ""
174 }
175 return sessionStem(sessionPath) + ".conflicts.jsonl"
176 }
177
178 // SessionLockFile is the advisory save lock (<id>.jsonl.lock).
179 func SessionLockFile(sessionPath string) string {
180 if sessionPath == "" {
181 return ""
182 }
183 return sessionPath + ".lock"
184 }
185
186 // SessionLeaseLock is the runtime ownership lock (<id>.jsonl.lease.lock).
187 func SessionLeaseLock(sessionPath string) string {
188 if sessionPath == "" {
189 return ""
190 }
191 return sessionPath + ".lease.lock"
192 }
193
194 // SessionLeaseInfo is the runtime ownership metadata
195 // (<id>.jsonl.lease.json).
196 func SessionLeaseInfo(sessionPath string) string {
197 if sessionPath == "" {
198 return ""
199 }
200 return sessionPath + ".lease.json"
201 }
202
203 // SessionCheckpointDir is the snapshot-checkpoint directory (<id>.ckpt).
204 func SessionCheckpointDir(sessionPath string) string {
205 if sessionPath == "" {
206 return ""
207 }
208 return sessionStem(sessionPath) + ".ckpt"
209 }
210
211 // SessionJobsDir is the background-job artifact directory (<id>.jobs).
212 func SessionJobsDir(sessionPath string) string {
213 sessionPath = strings.TrimSpace(sessionPath)
214 if sessionPath == "" {
215 return ""
216 }
217 return sessionStem(sessionPath) + ".jobs"
218 }
219
220 // SessionInboxDir is the durable session-level instruction inbox
221 // (<id>.inbox/). Manifest metadata and frozen prompt blobs live here.
222 func SessionInboxDir(sessionPath string) string {
223 sessionPath = strings.TrimSpace(sessionPath)
224 if sessionPath == "" {
225 return ""
226 }
227 return sessionStem(sessionPath) + ".inbox"
228 }
229
230 // SessionCleanupPending is the delayed-cleanup marker (<id>.cleanup-pending.json).
231 func SessionCleanupPending(sessionPath string) string {
232 sessionPath = strings.TrimSpace(sessionPath)
233 if sessionPath == "" {
234 return ""
235 }
236 return sessionStem(sessionPath) + ".cleanup-pending.json"
237 }
238
239 // SessionSidecarFiles returns every regular-file sidecar owned by a session
240 // transcript: branch meta, goal state, event/index logs, pinned context, and
241 // diagnostic logs.
242 // Every surface that deletes a session (desktop trash, /clear, serve, ACP)
243 // must remove all of these — the event log is the authoritative transcript, so
244 // leaving it behind both leaks the "deleted" conversation and lets LoadSession
245 // resurrect it. Directory artifacts (checkpoints, jobs) and ephemeral
246 // lock/lease files have their own lifecycles and are intentionally not listed.
247 func SessionSidecarFiles(sessionPath string) []string {
248 sessionPath = strings.TrimSpace(sessionPath)
249 if sessionPath == "" {
250 return nil
251 }
252 return []string{
253 SessionMeta(sessionPath),
254 SessionGoalState(sessionPath),
255 SessionEventLog(sessionPath),
256 SessionEventLogDamaged(sessionPath),
257 SessionEventLogRotating(sessionPath),
258 SessionTurnEventLog(sessionPath),
259 SessionTurnEventLogDamaged(sessionPath),
260 SessionEventIndex(sessionPath),
261 SessionDisplayIndex(sessionPath),
262 SessionTranscriptProjection(sessionPath),
263 SessionConflictLog(sessionPath),
264 SessionRecoveryState(sessionPath),
265 SessionContext(sessionPath),
266 SessionPinnedContext(sessionPath),
267 }
268 }
269
269 lines GO