返回 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, ".conflicts.jsonl") &&
28 !strings.HasSuffix(name, ".guardian.jsonl")
29 }
30
31 // SessionRecoveryState is the persisted Auto-mode recovery checkpoint state
32 // (<id>.recovery.json). It is a regular session-owned sidecar, not a transcript.
33 func SessionRecoveryState(sessionPath string) string {
34 sessionPath = strings.TrimSpace(sessionPath)
35 if sessionPath == "" {
36 return ""
37 }
38 return sessionStem(sessionPath) + ".recovery.json"
39 }
40
41 // sessionStem strips the .jsonl suffix so a sidecar sits beside the session as
42 // <id>.<kind> rather than <id>.jsonl.<kind>.
43 func sessionStem(sessionPath string) string {
44 return strings.TrimSuffix(sessionPath, ".jsonl")
45 }
46
47 // SessionMeta is the branch-metadata sidecar. Unlike the other sidecars it
48 // appends to the full session path (historical layout), so session.jsonl yields
49 // session.jsonl.meta.
50 func SessionMeta(sessionPath string) string {
51 if sessionPath == "" {
52 return ""
53 }
54 return sessionPath + ".meta"
55 }
56
57 // SessionGoalState is the persisted active-goal sidecar (<id>.goal-state.json).
58 func SessionGoalState(sessionPath string) string {
59 if sessionPath == "" {
60 return ""
61 }
62 return sessionStem(sessionPath) + ".goal-state.json"
63 }
64
65 // SessionEventLog is the append-only transcript event log (<id>.events.jsonl).
66 func SessionEventLog(sessionPath string) string {
67 if sessionPath == "" {
68 return ""
69 }
70 return sessionStem(sessionPath) + ".events.jsonl"
71 }
72
73 // SessionEventLogDamaged is the salvage sidecar for event-log bytes that tail
74 // repair would otherwise discard (<id>.events.jsonl.damaged). It must NOT end
75 // in .jsonl: older binaries scanning a shared session directory classify any
76 // non-excluded .jsonl file as a primary transcript and would resurrect the
77 // damaged bytes as a phantom session.
78 func SessionEventLogDamaged(sessionPath string) string {
79 if sessionPath == "" {
80 return ""
81 }
82 return SessionEventLog(sessionPath) + ".damaged"
83 }
84
85 // SessionEventIndex is the listing/checkpoint index for the event log
86 // (<id>.event-index.json). It contains derived offsets and digests, not the
87 // transcript body.
88 func SessionEventIndex(sessionPath string) string {
89 if sessionPath == "" {
90 return ""
91 }
92 return sessionStem(sessionPath) + ".event-index.json"
93 }
94
95 // SessionConflictLog is the append-only diagnostic log for snapshot conflict
96 // recoveries (<id>.conflicts.jsonl). It contains revision counters and branch
97 // ids, not transcript content.
98 func SessionConflictLog(sessionPath string) string {
99 if sessionPath == "" {
100 return ""
101 }
102 return sessionStem(sessionPath) + ".conflicts.jsonl"
103 }
104
105 // SessionLockFile is the advisory save lock (<id>.jsonl.lock).
106 func SessionLockFile(sessionPath string) string {
107 if sessionPath == "" {
108 return ""
109 }
110 return sessionPath + ".lock"
111 }
112
113 // SessionLeaseLock is the runtime ownership lock (<id>.jsonl.lease.lock).
114 func SessionLeaseLock(sessionPath string) string {
115 if sessionPath == "" {
116 return ""
117 }
118 return sessionPath + ".lease.lock"
119 }
120
121 // SessionLeaseInfo is the runtime ownership metadata
122 // (<id>.jsonl.lease.json).
123 func SessionLeaseInfo(sessionPath string) string {
124 if sessionPath == "" {
125 return ""
126 }
127 return sessionPath + ".lease.json"
128 }
129
130 // SessionCheckpointDir is the snapshot-checkpoint directory (<id>.ckpt).
131 func SessionCheckpointDir(sessionPath string) string {
132 if sessionPath == "" {
133 return ""
134 }
135 return sessionStem(sessionPath) + ".ckpt"
136 }
137
138 // SessionJobsDir is the background-job artifact directory (<id>.jobs).
139 func SessionJobsDir(sessionPath string) string {
140 sessionPath = strings.TrimSpace(sessionPath)
141 if sessionPath == "" {
142 return ""
143 }
144 return sessionStem(sessionPath) + ".jobs"
145 }
146
147 // SessionCleanupPending is the delayed-cleanup marker (<id>.cleanup-pending.json).
148 func SessionCleanupPending(sessionPath string) string {
149 sessionPath = strings.TrimSpace(sessionPath)
150 if sessionPath == "" {
151 return ""
152 }
153 return sessionStem(sessionPath) + ".cleanup-pending.json"
154 }
155
156 // SessionSidecarFiles returns every regular-file sidecar owned by a session
157 // transcript: branch meta, goal state, event/index logs, and diagnostic logs.
158 // Every surface that deletes a session (desktop trash, /clear, serve, ACP)
159 // must remove all of these — the event log is the authoritative transcript, so
160 // leaving it behind both leaks the "deleted" conversation and lets LoadSession
161 // resurrect it. Directory artifacts (checkpoints, jobs) and ephemeral
162 // lock/lease files have their own lifecycles and are intentionally not listed.
163 func SessionSidecarFiles(sessionPath string) []string {
164 sessionPath = strings.TrimSpace(sessionPath)
165 if sessionPath == "" {
166 return nil
167 }
168 return []string{
169 SessionMeta(sessionPath),
170 SessionGoalState(sessionPath),
171 SessionEventLog(sessionPath),
172 SessionEventLogDamaged(sessionPath),
173 SessionEventIndex(sessionPath),
174 SessionConflictLog(sessionPath),
175 SessionRecoveryState(sessionPath),
176 }
177 }
178
178 lines GO