返回 DeepSeek-Reasonix
session_dag.go
根目录 / internal / agent / session_dag.go
1 package agent
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "time"
8
9 "reasonix/internal/provider"
10 )
11
12 // Schema 2 of <id>.events.jsonl is an append-only DAG: every message entry
13 // names its parent, heads are named pointers into that graph, and every other
14 // operation (rewind, fork, redaction, compaction, turn boundaries) is a marker
15 // appended behind the messages it refers to. Nothing rewrites earlier bytes
16 // except a generation rotation under a single-writer proof. Unknown entry
17 // types are a hard error, so the whole vocabulary ships with the schema.
18 const (
19 sessionDAGSchemaVersion = 2
20
21 sessionDAGTypeLog = "log"
22 sessionDAGTypeMessage = "message"
23 sessionDAGTypePatch = "patch"
24 sessionDAGTypeSystem = "system"
25 sessionDAGTypeFork = "fork"
26 sessionDAGTypeRewind = "rewind"
27 sessionDAGTypeSelect = "select"
28 sessionDAGTypeRename = "rename"
29 sessionDAGTypeRetire = "retire"
30 sessionDAGTypeTurnBegin = "turn_begin"
31 sessionDAGTypeTurnEnd = "turn_end"
32 sessionDAGTypeCompaction = "compaction"
33 sessionDAGTypeRedact = "redact"
34 sessionDAGTypeWriter = "writer"
35 sessionDAGTypeCheckpoint = "checkpoint"
36
37 // SessionMainHead is the head every upgraded or freshly created log starts
38 // with; forks, rewinds, and concurrent writers mint new head ids.
39 SessionMainHead = "main"
40
41 HeadKindMain = "main"
42 HeadKindFork = "fork"
43 HeadKindRewind = "rewind"
44 HeadKindConcurrent = "concurrent"
45 )
46
47 // HeadRef is a writer's position in a schema-2 log: the head it extends, that
48 // head's leaf message, and the log generation/offset it last observed.
49 type HeadRef struct {
50 HeadID string
51 LeafID string
52 LogGeneration int64
53 LogOffset int64
54 }
55
56 // SessionHead describes one head of a schema-2 log for listings, the versions
57 // UI, and the catalog. MessageCount is the length of the materialized chain.
58 type SessionHead struct {
59 ID string `json:"id"`
60 Kind string `json:"kind"`
61 Name string `json:"name,omitempty"`
62 ParentHead string `json:"parent_head,omitempty"`
63 ForkFrom string `json:"fork_from,omitempty"`
64 Writer string `json:"writer,omitempty"`
65 LeafID string `json:"leaf,omitempty"`
66 CreatedAt time.Time `json:"created_at"`
67 LastActivity time.Time `json:"last_activity"`
68 Retired bool `json:"retired,omitempty"`
69 Selected bool `json:"selected,omitempty"`
70 Covered bool `json:"covered,omitempty"` // live, unselected, adds nothing beyond the selected chain
71 MessageCount int `json:"message_count"`
72 Turns int `json:"turns,omitempty"`
73 Preview string `json:"preview,omitempty"`
74 }
75
76 type sessionDAGOrigin struct {
77 Session string `json:"session,omitempty"`
78 Entry string `json:"entry,omitempty"`
79 }
80
81 // sessionDAGEntry is the wire shape shared by every entry type; the header
82 // fields come first so the 4 KiB probe finds schema_version and type ahead of
83 // any image-bearing payload. Msgs always holds exactly one message.
84 type sessionDAGEntry struct {
85 SchemaVersion int `json:"schema_version"`
86 Type string `json:"type"`
87 ID string `json:"id,omitempty"`
88 Head string `json:"head,omitempty"`
89 Writer string `json:"writer,omitempty"`
90 Turn string `json:"turn,omitempty"`
91 At time.Time `json:"at"`
92
93 Parent string `json:"parent,omitempty"`
94 Digest string `json:"digest,omitempty"`
95 Msgs json.RawMessage `json:"msgs,omitempty"`
96
97 Target string `json:"target,omitempty"`
98
99 NewHead string `json:"new_head,omitempty"`
100 From string `json:"from,omitempty"`
101 Kind string `json:"kind,omitempty"`
102 Name string `json:"name,omitempty"`
103
104 To string `json:"to,omitempty"`
105 Cause string `json:"cause,omitempty"`
106 Reason string `json:"reason,omitempty"`
107
108 Leaf string `json:"leaf,omitempty"`
109 PreserveUser bool `json:"preserve_user,omitempty"`
110
111 CoveredLeaf string `json:"covered_leaf,omitempty"`
112 CoveredCount int `json:"covered_count,omitempty"`
113 PrefixHash string `json:"prefix_hash,omitempty"`
114
115 Targets map[string]json.RawMessage `json:"targets,omitempty"`
116
117 PID int `json:"pid,omitempty"`
118 Hostname string `json:"hostname,omitempty"`
119 LeaseGeneration uint64 `json:"lease_generation,omitempty"`
120
121 Generation int64 `json:"generation,omitempty"`
122 RotatedFrom int64 `json:"rotated_from,omitempty"`
123 UpgradedFrom int `json:"upgraded_from_schema,omitempty"`
124 Origin *sessionDAGOrigin `json:"origin,omitempty"`
125
126 SelectedHead string `json:"selected_head,omitempty"`
127 Heads []SessionHead `json:"heads,omitempty"`
128 Dropped []string `json:"dropped,omitempty"`
129 Tombstones []string `json:"tombstones,omitempty"`
130 }
131
132 // NewHeadID mints a head id with the same time-prefixed layout as message ids.
133 func NewHeadID() string {
134 return NewMessageID()
135 }
136
137 // sessionDAGChainDigest is the per-entry hash chain: sha256(parent digest,
138 // identity JSON). Unlike the flat transcript digest it can be extended from
139 // any ancestor, which is what a fork needs.
140 func sessionDAGChainDigest(parentDigest string, m provider.Message) (string, error) {
141 b, err := json.Marshal(messageForSessionIdentity(m))
142 if err != nil {
143 return "", err
144 }
145 h := sha256.New()
146 h.Write([]byte(parentDigest))
147 h.Write([]byte{0})
148 h.Write(b)
149 return hex.EncodeToString(h.Sum(nil)), nil
150 }
151
152 func encodeSessionDAGMessage(m provider.Message) (json.RawMessage, error) {
153 b, err := json.Marshal([]provider.Message{m})
154 if err != nil {
155 return nil, err
156 }
157 return b, nil
158 }
159
160 // newSessionDAGMessageEntry builds a message entry for m under head with the
161 // given parent; the chain digest is derived from the parent's digest.
162 func newSessionDAGMessageEntry(head, parent, parentDigest, turn string, m provider.Message, at time.Time) (sessionDAGEntry, error) {
163 raw, err := encodeSessionDAGMessage(m)
164 if err != nil {
165 return sessionDAGEntry{}, err
166 }
167 digest, err := sessionDAGChainDigest(parentDigest, m)
168 if err != nil {
169 return sessionDAGEntry{}, err
170 }
171 return sessionDAGEntry{
172 Type: sessionDAGTypeMessage,
173 ID: m.ID,
174 Head: head,
175 Turn: turn,
176 At: at,
177 Parent: parent,
178 Digest: digest,
179 Msgs: raw,
180 }, nil
181 }
182
182 lines GO