返回 DeepSeek-Reasonix
session_listing_helpers.go
根目录 / internal / agent / session_listing_helpers.go
1 package agent
2
3 import (
4 "os"
5 "strings"
6 "time"
7 )
8
9 // SessionOrderInfo is the lightweight sidecar/mtime ordering record shared by
10 // session pickers and prompt-history navigation. It intentionally avoids JSONL.
11 type SessionOrderInfo struct {
12 Path string
13 CreatedAt time.Time
14 LastActivityAt time.Time
15 ModTime time.Time // compatibility alias for LastActivityAt
16 Scope string
17 WorkspaceRoot string
18 TopicID string
19 TopicTitle string
20 CustomTitle string
21 Recovered bool
22 VersionKind SessionVersionKind
23 VersionState SessionVersionState
24 ParentConversationID string
25 ParentVersionID string
26 RecoveryReason string
27 RecoveryDigest string
28 ParentID string
29 RecoveryPreferred bool
30 // Counts are trusted only when their listing identity matches the transcript.
31 Turns int
32 Preview string
33 SchemaVersion int
34 // The transcript and listing identities also fence async listing backfills.
35 Revision int64
36 ContentDigest string
37 ListingRevision int64
38 ListingContentDigest string
39 // Schema-2 sessions mirror their selected head here so listings never
40 // replay the log; LogSchema is 0 for schema-1 sidecars.
41 HeadID string
42 HeadCount int
43 LogSchema int
44 }
45
46 // RecoveryPreferenceResolver validates one explicit recovery preference while
47 // ListSessionOrder is assembling its metadata-only result.
48 type RecoveryPreferenceResolver func(path string, meta BranchMeta) bool
49
50 // ListingProjectionFresh reports whether Turns/Preview describe this record's
51 // current transcript generation. Legacy sidecars that predate the content
52 // ledger remain usable until their first revisioned save.
53 func (s SessionOrderInfo) ListingProjectionFresh() bool {
54 return sessionListingProjectionFresh(s.SchemaVersion, s.Turns, s.Revision, s.ListingRevision, s.ContentDigest, s.ListingContentDigest)
55 }
56
57 func sessionInfoFromOrder(session SessionOrderInfo, preview string, turns int, countsKnown bool) SessionInfo {
58 return SessionInfo{
59 Path: session.Path,
60 CreatedAt: session.CreatedAt,
61 LastActivityAt: session.LastActivityAt,
62 ModTime: session.ModTime,
63 Preview: preview,
64 Turns: turns,
65 CountsKnown: countsKnown,
66 Scope: session.Scope,
67 WorkspaceRoot: session.WorkspaceRoot,
68 TopicID: session.TopicID,
69 TopicTitle: session.TopicTitle,
70 CustomTitle: session.CustomTitle,
71 Recovered: session.Recovered,
72 VersionKind: session.VersionKind,
73 VersionState: session.VersionState,
74 ParentConversationID: session.ParentConversationID,
75 ParentVersionID: session.ParentVersionID,
76 RecoveryReason: session.RecoveryReason,
77 RecoveryDigest: session.RecoveryDigest,
78 ParentID: session.ParentID,
79 }
80 }
81
82 func sessionArtifactsHaveContent(path string) bool {
83 if info, err := os.Stat(path); err == nil && !info.IsDir() && info.Size() > 0 {
84 return true
85 }
86 return sessionEventLogSize(path) > 0
87 }
88
89 func sessionListingProjectionFresh(schemaVersion, turns int, revision, listingRevision int64, contentDigest, listingContentDigest string) bool {
90 if schemaVersion < branchMetaCountsInitialVersion {
91 return false
92 }
93 if turns == 0 && schemaVersion < BranchMetaCountsVersion {
94 return false
95 }
96 contentDigest = strings.TrimSpace(contentDigest)
97 listingContentDigest = strings.TrimSpace(listingContentDigest)
98 if revision == 0 && listingRevision == 0 && contentDigest == "" && listingContentDigest == "" {
99 return true
100 }
101 return revision > 0 && listingRevision == revision && contentDigest != "" && listingContentDigest == contentDigest
102 }
103
104 func stampSessionListingProjection(meta *BranchMeta) {
105 if meta == nil {
106 return
107 }
108 meta.ListingRevision = meta.Revision
109 meta.ListingContentDigest = strings.TrimSpace(meta.ContentDigest)
110 }
111
112 func updateSessionListingCountsIfCurrent(session SessionOrderInfo, preview string, turns int) (string, int, error) {
113 unlock, err := LockSessionMetaPath(session.Path)
114 if err != nil {
115 return preview, turns, err
116 }
117 defer unlock()
118
119 current, err := ensureBranchMetaUnlocked(session.Path)
120 if err != nil {
121 return preview, turns, err
122 }
123 if current.SchemaVersion != session.SchemaVersion ||
124 current.Turns != session.Turns ||
125 current.Preview != session.Preview ||
126 current.Revision != session.Revision ||
127 current.ContentDigest != session.ContentDigest {
128 return current.Preview, current.Turns, nil
129 }
130 if sessionListingProjectionFresh(current.SchemaVersion, current.Turns, current.Revision, current.ListingRevision, current.ContentDigest, current.ListingContentDigest) {
131 return current.Preview, current.Turns, nil
132 }
133
134 current.Preview = preview
135 current.Turns = turns
136 current.SchemaVersion = BranchMetaCountsVersion
137 stampSessionListingProjection(&current)
138 if err := saveBranchMeta(session.Path, current, false); err != nil {
139 return preview, turns, err
140 }
141 return preview, turns, nil
142 }
143
144 func previewSessionWithError(path string) (string, int, error) {
145 msgs, _, _, err := loadSessionMessages(path)
146 if err != nil {
147 return "", 0, err
148 }
149 preview, turns := SessionPreviewFromMessages(msgs)
150 return preview, turns, nil
151 }
152
152 lines GO