返回 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 if !strings.HasSuffix(name, ".jsonl") {
26 return false
27 }
28 for _, suffix := range sessionJSONLSidecars {
29 if strings.HasSuffix(name, suffix) {
30 return false
31 }
32 }
33 return true
34 }
35
36 // sessionJSONLSidecars end in .jsonl beside a transcript without being one.
37 // The last three are written by Reasonix 2.x, which shares this directory.
38 var sessionJSONLSidecars = []string{
39 ".events.jsonl", ".turns.jsonl", ".conflicts.jsonl", ".guardian.jsonl",
40 ".wire.jsonl", ".adjudication.jsonl", ".execution.jsonl",
41 }
42
43 // SessionRecoveryState is the persisted Auto-mode recovery checkpoint state
44 // (<id>.recovery.json). It is a regular session-owned sidecar, not a transcript.
45 func SessionRecoveryState(sessionPath string) string {
46 sessionPath = strings.TrimSpace(sessionPath)
47 if sessionPath == "" {
48 return ""
49 }
50 return sessionStem(sessionPath) + ".recovery.json"
51 }
52
53 // SessionTranscriptProjection stores display-only records and their exact
54 // committed event coverage; it never supplies provider-visible messages.
55 func SessionTranscriptProjection(sessionPath string) string {
56 if strings.TrimSpace(sessionPath) == "" {
57 return ""
58 }
59 return sessionStem(sessionPath) + ".transcript-projection.json"
60 }
61
62 // SessionContext is the context-projection / compaction-state sidecar
63 // (<id>.context.json). It holds the model-visible projection and cache
64 // telemetry; transcript authority remains with the native event log once one
65 // exists, with the primary .jsonl retained as its compatibility checkpoint.
66 func SessionContext(sessionPath string) string {
67 sessionPath = strings.TrimSpace(sessionPath)
68 if sessionPath == "" {
69 return ""
70 }
71 return sessionStem(sessionPath) + ".context.json"
72 }
73
74 // SessionPinnedContext is the optional desktop pinned-workspace-context
75 // sidecar (<id>.pinned-context.json). Older versions ignore it while keeping
76 // the primary transcript fully readable.
77 func SessionPinnedContext(sessionPath string) string {
78 sessionPath = strings.TrimSpace(sessionPath)
79 if sessionPath == "" {
80 return ""
81 }
82 return sessionStem(sessionPath) + ".pinned-context.json"
83 }
84
85 // sessionStem strips the .jsonl suffix so a sidecar sits beside the session as
86 // <id>.<kind> rather than <id>.jsonl.<kind>.
87 func sessionStem(sessionPath string) string {
88 return strings.TrimSuffix(sessionPath, ".jsonl")
89 }
90
91 // SessionMeta is the branch-metadata sidecar. Unlike the other sidecars it
92 // appends to the full session path (historical layout), so session.jsonl yields
93 // session.jsonl.meta.
94 func SessionMeta(sessionPath string) string {
95 if sessionPath == "" {
96 return ""
97 }
98 return sessionPath + ".meta"
99 }
100
101 // SessionGoalState is the persisted active-goal sidecar (<id>.goal-state.json).
102 func SessionGoalState(sessionPath string) string {
103 if sessionPath == "" {
104 return ""
105 }
106 return sessionStem(sessionPath) + ".goal-state.json"
107 }
108
109 // SessionEventLog is the append-only transcript event log (<id>.events.jsonl).
110 func SessionEventLog(sessionPath string) string {
111 if sessionPath == "" {
112 return ""
113 }
114 return sessionStem(sessionPath) + ".events.jsonl"
115 }
116
117 // SessionEventLogDamaged is the salvage sidecar for event-log bytes that tail
118 // repair would otherwise discard (<id>.events.jsonl.damaged). It must NOT end
119 // in .jsonl: older binaries scanning a shared session directory classify any
120 // non-excluded .jsonl file as a primary transcript and would resurrect the
121 // damaged bytes as a phantom session.
122 func SessionEventLogDamaged(sessionPath string) string {
123 if sessionPath == "" {
124 return ""
125 }
126 return SessionEventLog(sessionPath) + ".damaged"
127 }
128
129 // SessionEventLogRotating marks a schema-2 log whose rotation is between
130 // reading the old file and publishing the new one; unlocked appenders wait
131 // for it to clear before trusting that their bytes reached the current log.
132 func SessionEventLogRotating(sessionPath string) string {
133 if sessionPath == "" {
134 return ""
135 }
136 return SessionEventLog(sessionPath) + ".rotating"
137 }
138
139 // SessionTurnEventLog is the append-only local runtime lifecycle ledger
140 // (<id>.turns.jsonl). It is independent from the provider transcript so old
141 // readers can continue to consume the primary session unchanged.
142 func SessionTurnEventLog(sessionPath string) string {
143 if sessionPath == "" {
144 return ""
145 }
146 return sessionStem(sessionPath) + ".turns.jsonl"
147 }
148
149 // SessionTurnEventLogDamaged preserves a corrupt/torn ledger tail before the
150 // valid prefix is truncated back into service.
151 func SessionTurnEventLogDamaged(sessionPath string) string {
152 if sessionPath == "" {
153 return ""
154 }
155 return SessionTurnEventLog(sessionPath) + ".damaged"
156 }
157
158 // SessionEventIndex is the listing/checkpoint index for the event log
159 // (<id>.event-index.json). It contains derived offsets and digests, not the
160 // transcript body.
161 func SessionEventIndex(sessionPath string) string {
162 if sessionPath == "" {
163 return ""
164 }
165 return sessionStem(sessionPath) + ".event-index.json"
166 }
167
168 // SessionDisplayIndex is the paging sidecar for the transcript
169 // (<id>.display-index.json). It contains per-message byte offsets, roles, and
170 // turn boundaries derived from the transcript, never message bodies, so a
171 // reader can page a huge history without parsing whole session files.
172 func SessionDisplayIndex(sessionPath string) string {
173 if sessionPath == "" {
174 return ""
175 }
176 return sessionStem(sessionPath) + ".display-index.json"
177 }
178
179 // SessionConflictLog is the append-only diagnostic log for snapshot conflict
180 // recoveries (<id>.conflicts.jsonl). It contains revision counters and branch
181 // ids, not transcript content.
182 func SessionConflictLog(sessionPath string) string {
183 if sessionPath == "" {
184 return ""
185 }
186 return sessionStem(sessionPath) + ".conflicts.jsonl"
187 }
188
189 // SessionLockFile is the advisory save lock (<id>.jsonl.lock).
190 func SessionLockFile(sessionPath string) string {
191 if sessionPath == "" {
192 return ""
193 }
194 return sessionPath + ".lock"
195 }
196
197 // SessionLeaseLock is the runtime ownership lock (<id>.jsonl.lease.lock).
198 func SessionLeaseLock(sessionPath string) string {
199 if sessionPath == "" {
200 return ""
201 }
202 return sessionPath + ".lease.lock"
203 }
204
205 // SessionLeaseInfo is the runtime ownership metadata
206 // (<id>.jsonl.lease.json).
207 func SessionLeaseInfo(sessionPath string) string {
208 if sessionPath == "" {
209 return ""
210 }
211 return sessionPath + ".lease.json"
212 }
213
214 // SessionCheckpointDir is the snapshot-checkpoint directory (<id>.ckpt).
215 func SessionCheckpointDir(sessionPath string) string {
216 if sessionPath == "" {
217 return ""
218 }
219 return sessionStem(sessionPath) + ".ckpt"
220 }
221
222 // SessionJobsDir is the background-job artifact directory (<id>.jobs).
223 func SessionJobsDir(sessionPath string) string {
224 sessionPath = strings.TrimSpace(sessionPath)
225 if sessionPath == "" {
226 return ""
227 }
228 return sessionStem(sessionPath) + ".jobs"
229 }
230
231 // SessionInboxDir is the durable session-level instruction inbox
232 // (<id>.inbox/). Manifest metadata and frozen prompt blobs live here.
233 func SessionInboxDir(sessionPath string) string {
234 sessionPath = strings.TrimSpace(sessionPath)
235 if sessionPath == "" {
236 return ""
237 }
238 return sessionStem(sessionPath) + ".inbox"
239 }
240
241 // SessionCleanupPending is the delayed-cleanup marker (<id>.cleanup-pending.json).
242 func SessionCleanupPending(sessionPath string) string {
243 sessionPath = strings.TrimSpace(sessionPath)
244 if sessionPath == "" {
245 return ""
246 }
247 return sessionStem(sessionPath) + ".cleanup-pending.json"
248 }
249
250 // SessionSidecarFiles returns every regular-file sidecar owned by a session
251 // transcript: branch meta, goal state, event/index logs, pinned context, and
252 // diagnostic logs.
253 // Every surface that deletes a session (desktop trash, /clear, serve, ACP)
254 // must remove all of these — the event log is the authoritative transcript, so
255 // leaving it behind both leaks the "deleted" conversation and lets LoadSession
256 // resurrect it. Directory artifacts (checkpoints, jobs) and ephemeral
257 // lock/lease files have their own lifecycles and are intentionally not listed.
258 func SessionSidecarFiles(sessionPath string) []string {
259 sessionPath = strings.TrimSpace(sessionPath)
260 if sessionPath == "" {
261 return nil
262 }
263 return []string{
264 SessionMeta(sessionPath),
265 SessionGoalState(sessionPath),
266 SessionEventLog(sessionPath),
267 SessionEventLogDamaged(sessionPath),
268 SessionEventLogRotating(sessionPath),
269 SessionTurnEventLog(sessionPath),
270 SessionTurnEventLogDamaged(sessionPath),
271 SessionEventIndex(sessionPath),
272 SessionDisplayIndex(sessionPath),
273 SessionTranscriptProjection(sessionPath),
274 SessionConflictLog(sessionPath),
275 SessionRecoveryState(sessionPath),
276 SessionContext(sessionPath),
277 SessionPinnedContext(sessionPath),
278 }
279 }
280
280 lines GO