返回 DeepSeek-Reasonix
session_ids.go
根目录 / internal / agent / session_ids.go
1 package agent
2
3 import (
4 "crypto/rand"
5 "crypto/sha256"
6 "encoding/binary"
7 "encoding/json"
8 "slices"
9 "strconv"
10 "time"
11
12 "reasonix/internal/provider"
13 )
14
15 // Message ids are 128 bits in Crockford base32 (26 chars, ULID layout for new
16 // ids). They never reach a provider and never enter transcript digests, so a
17 // history with or without ids hashes and serializes identically on the wire.
18 const (
19 messageIDLen = 26
20 messageIDAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
21 legacyMessageIDNS = "reasonix-legacy-entry-v1"
22 )
23
24 // NewMessageID mints a time-prefixed random id: 48 bits of unix milliseconds
25 // followed by 80 random bits, so ids sort roughly by creation while staying
26 // unguessable across processes.
27 func NewMessageID() string {
28 var raw [16]byte
29 ms := uint64(time.Now().UnixMilli())
30 binary.BigEndian.PutUint64(raw[:8], ms<<16)
31 if _, err := rand.Read(raw[6:]); err != nil {
32 binary.BigEndian.PutUint64(raw[8:], uint64(time.Now().UnixNano()))
33 }
34 return encodeMessageID(raw)
35 }
36
37 func encodeMessageID(raw [16]byte) string {
38 var out [messageIDLen]byte
39 hi := binary.BigEndian.Uint64(raw[:8])
40 lo := binary.BigEndian.Uint64(raw[8:])
41 for i := messageIDLen - 1; i >= 0; i-- {
42 out[i] = messageIDAlphabet[lo&31]
43 lo = lo>>5 | hi<<59
44 hi >>= 5
45 }
46 return string(out[:])
47 }
48
49 // legacyMessageID derives the id of a message that predates ids. The running
50 // transcript digest binds it to everything before it, so two processes loading
51 // the same bytes agree and a moved or edited prefix cannot alias an id.
52 func legacyMessageID(branchID string, index int, running []byte) string {
53 h := sha256.New()
54 h.Write([]byte(legacyMessageIDNS))
55 h.Write([]byte{0})
56 h.Write([]byte(branchID))
57 h.Write([]byte{0})
58 h.Write([]byte(strconv.Itoa(index)))
59 h.Write([]byte{0})
60 h.Write(running)
61 var raw [16]byte
62 copy(raw[:], h.Sum(nil))
63 return encodeMessageID(raw)
64 }
65
66 // assignLegacyMessageIDs fills in ids for messages loaded from storage that
67 // carries none. Messages that already have an id keep it; the running digest
68 // mirrors sessionTranscriptHasher so it is independent of the ids themselves.
69 func assignLegacyMessageIDs(path string, msgs []provider.Message) {
70 branchID := BranchID(path)
71 h := sha256.New()
72 for i := range msgs {
73 b, err := json.Marshal(messageForSessionIdentity(msgs[i]))
74 if err != nil {
75 return
76 }
77 h.Write(b)
78 h.Write([]byte{'\n'})
79 if msgs[i].ID == "" {
80 msgs[i].ID = legacyMessageID(branchID, i, h.Sum(nil))
81 }
82 }
83 }
84
85 func mintMessageIDs(msgs []provider.Message) {
86 for i := range msgs {
87 if msgs[i].ID == "" {
88 msgs[i].ID = NewMessageID()
89 }
90 }
91 }
92
93 // LeafID reports the id of the newest message, or "" for an empty session.
94 func (s *Session) LeafID() string {
95 if s == nil {
96 return ""
97 }
98 s.mu.RLock()
99 defer s.mu.RUnlock()
100 if len(s.Messages) == 0 {
101 return ""
102 }
103 return s.Messages[len(s.Messages)-1].ID
104 }
105
106 // IndexOfID resolves a message id to its current position, or -1. It scans
107 // from the tail because callers almost always ask about recent messages.
108 func (s *Session) IndexOfID(id string) int {
109 if s == nil || id == "" {
110 return -1
111 }
112 s.mu.RLock()
113 defer s.mu.RUnlock()
114 for i, m := range slices.Backward(s.Messages) {
115 if m.ID == id {
116 return i
117 }
118 }
119 return -1
120 }
121
121 lines GO