返回 DeepSeek-Reasonix
save_listing_projection.go
根目录 / internal / agent / save_listing_projection.go
1 package agent
2
3 import (
4 "crypto/sha256"
5 "fmt"
6 "log/slog"
7 "math"
8 "strings"
9
10 "reasonix/internal/provider"
11 )
12
13 func (s *Session) classifySnapshotWriteForCommit(path string, msgs []provider.Message, digest [sha256.Size]byte, version uint64, ownedRewrite bool, mode sessionSaveMode) (snapshotWriteDecision, error) {
14 decision, err := s.classifySnapshotWrite(path, msgs, digest, version, ownedRewrite)
15 if err != nil || decision.upToDate && mode != sessionSaveRewriteCompact && !decision.ledgerStale {
16 return decision, err
17 }
18 // Invalidate before any transcript mutation or stale-ledger repair so an
19 // interrupted commit cannot leave the previous listing self-certified.
20 reservedRevision, err := invalidateSessionListingProjection(path)
21 if err != nil {
22 return decision, fmt.Errorf("invalidate session listing projection: %w", err)
23 }
24 decision.reservedRevision = reservedRevision
25 return decision, nil
26 }
27
28 func (s *Session) markPersistedWithListing(path string, digest [sha256.Size]byte, version uint64, revision int64, rewriteVersion int, msgs []provider.Message) {
29 // Pair the persisted-message view with the baseline: writer-bound saves
30 // use it to classify append shapes without reloading the transcript.
31 s.setPersistedBaseline(path, digest, version, revision, true, true, rewriteVersion, msgs)
32 persistSessionListingProjection(path, msgs, revision, digestString(digest))
33 }
34
35 // invalidateSessionListingProjection makes cached counts untrusted before a
36 // transcript commit begins. If the transcript lands but its revision ledger
37 // does not, readers must decode/repair instead of certifying the previous
38 // generation's preview from an internally consistent but stale sidecar.
39 func invalidateSessionListingProjection(path string) (int64, error) {
40 if !sessionArtifactsHaveContent(path) {
41 // A brand-new session has no prior projection to reuse. It also needs to
42 // retain the historical first committed revision of one.
43 return 0, nil
44 }
45 var reservedRevision int64
46 err := UpdateBranchMeta(path, false, func(meta *BranchMeta) error {
47 // Reserve the next transcript generation so stale whole-sidecar writers
48 // preserve this invalidation. The transcript commit finalizes the same
49 // revision, keeping the WAL and content ledger in one generation.
50 if meta.Revision == math.MaxInt64 {
51 return fmt.Errorf("session revision exhausted")
52 }
53 meta.Revision++
54 reservedRevision = meta.Revision
55 meta.WriterID = SessionWriterID()
56 meta.SchemaVersion = 0
57 return nil
58 })
59 return reservedRevision, err
60 }
61
62 func persistSessionListingProjection(path string, msgs []provider.Message, revision int64, contentDigest string) {
63 preview, turns := SessionPreviewFromMessages(msgs)
64 if err := UpdateBranchMeta(path, false, func(meta *BranchMeta) error {
65 // The transcript commit and this repairable projection are separate
66 // critical sections. A newer writer may already have advanced the
67 // sidecar, so only publish counts for the generation this save committed.
68 contentDigest = strings.TrimSpace(contentDigest)
69 if revision <= 0 || meta.Revision != revision || strings.TrimSpace(meta.ContentDigest) != contentDigest {
70 return nil
71 }
72 meta.Preview = preview
73 meta.Turns = turns
74 meta.SchemaVersion = BranchMetaCountsVersion
75 meta.ListingRevision = revision
76 meta.ListingContentDigest = contentDigest
77 return nil
78 }); err != nil {
79 // JSONL/event log already committed. Listing metadata is a repairable
80 // projection and must never turn a successful transcript save into an
81 // application error.
82 slog.Warn("session: listing metadata update deferred", "path", path, "err", err)
83 }
84 }
85
85 lines GO