返回 DeepSeek-Reasonix
heads.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "database/sql"
6 "fmt"
7
8 "reasonix/internal/agent"
9 )
10
11 // HeadRecord is one head of a schema-2 session as the catalog projects it.
12 // Rows come from the kernel's head index sidecar, never from replaying a log.
13 type HeadRecord struct {
14 Path string `json:"path"`
15 ID string `json:"id"`
16 ParentHeadID string `json:"parentHeadId,omitempty"`
17 Kind string `json:"kind"`
18 Name string `json:"name,omitempty"`
19 LeafMessageID string `json:"leafMessageId,omitempty"`
20 WriterID string `json:"writerId,omitempty"`
21 LastActivityAt int64 `json:"lastActivityAt,omitempty"`
22 Turns int `json:"turns"`
23 Preview string `json:"preview,omitempty"`
24 Retired bool `json:"retired,omitempty"`
25 Selected bool `json:"selected,omitempty"`
26 }
27
28 // sessionHeadProjection is what recordFromOrder learns about a schema-2
29 // session without opening its log: the sidecar mirror is always available,
30 // the per-head rows only while the head index still matches the log.
31 type sessionHeadProjection struct {
32 logFormat int
33 headCount int
34 selected string
35 fingerprint string
36 heads []HeadRecord
37 stale bool
38 }
39
40 func projectSessionHeads(info agent.SessionOrderInfo) sessionHeadProjection {
41 if info.LogSchema < 2 {
42 return sessionHeadProjection{logFormat: 1}
43 }
44 out := sessionHeadProjection{logFormat: info.LogSchema, headCount: info.HeadCount, selected: info.HeadID}
45 idx, err := agent.ReadSessionHeadIndex(info.Path)
46 if err != nil || idx == nil || !idx.Current(info.Path) {
47 out.stale = true
48 return out
49 }
50 out.selected = idx.SelectedHead
51 out.headCount = len(idx.Heads)
52 out.heads = make([]HeadRecord, 0, len(idx.Heads))
53 for _, h := range idx.Heads {
54 out.heads = append(out.heads, HeadRecord{
55 Path: info.Path, ID: h.ID, ParentHeadID: h.ParentHead, Kind: h.Kind, Name: h.Name,
56 LeafMessageID: h.LeafID, WriterID: h.Writer, LastActivityAt: unixMilli(h.LastActivity),
57 Turns: h.Turns, Preview: h.Preview, Retired: h.Retired, Selected: h.ID == idx.SelectedHead,
58 })
59 if h.ID == idx.SelectedHead {
60 out.fingerprint = "|h:" + h.ID + ":" + h.LeafID
61 }
62 }
63 return out
64 }
65
66 // writeDirectoryRow lands one scanned session row and its head rows inside
67 // the directory projection transaction.
68 func (c *Catalog) writeDirectoryRow(ctx context.Context, tx *sql.Tx, stmt *sql.Stmt, record SessionRecord, pathKey, directoryKey string, generation int64) error {
69 if _, err := stmt.ExecContext(ctx, c.sessionRowValues(record, pathKey, directoryKey, generation)...); err != nil {
70 return err
71 }
72 return upsertHeadRows(ctx, tx, pathKey, record.heads)
73 }
74
75 // upsertHeadRows replaces the head rows of one session inside the projection
76 // transaction. Stale projections keep the previous rows until the head index
77 // is current again.
78 func upsertHeadRows(ctx context.Context, tx *sql.Tx, pathKey string, heads []HeadRecord) error {
79 if heads == nil {
80 return nil
81 }
82 if _, err := tx.ExecContext(ctx, `DELETE FROM catalog_heads WHERE path_key=?`, pathKey); err != nil {
83 return err
84 }
85 for _, h := range heads {
86 if _, err := tx.ExecContext(ctx, `INSERT INTO catalog_heads(path_key,head_id,parent_head_id,kind,name,leaf_message_id,
87 writer_id,last_activity_at,turns,preview,retired,selected) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`,
88 pathKey, h.ID, h.ParentHeadID, h.Kind, h.Name, h.LeafMessageID, h.WriterID, h.LastActivityAt, h.Turns, h.Preview,
89 boolToInt(h.Retired), boolToInt(h.Selected)); err != nil {
90 return err
91 }
92 }
93 return nil
94 }
95
96 // ListHeads returns the projected heads of one session in creation order. A
97 // schema-1 session has none and returns an empty slice.
98 func (c *Catalog) ListHeads(ctx context.Context, path string) ([]HeadRecord, error) {
99 out := []HeadRecord{}
100 if c == nil || path == "" {
101 return out, nil
102 }
103 rows, err := c.db.QueryContext(ctx, `SELECT head_id,parent_head_id,kind,name,leaf_message_id,writer_id,last_activity_at,
104 turns,preview,retired,selected FROM catalog_heads WHERE path_key=? ORDER BY rowid`, c.pathKey(path))
105 if err != nil {
106 return out, fmt.Errorf("list session heads: %w", err)
107 }
108 defer rows.Close()
109 for rows.Next() {
110 var h HeadRecord
111 var retired, selected int
112 if err := rows.Scan(&h.ID, &h.ParentHeadID, &h.Kind, &h.Name, &h.LeafMessageID, &h.WriterID, &h.LastActivityAt,
113 &h.Turns, &h.Preview, &retired, &selected); err != nil {
114 return out, err
115 }
116 h.Path = path
117 h.Retired, h.Selected = retired != 0, selected != 0
118 out = append(out, h)
119 }
120 return out, rows.Err()
121 }
122
122 lines GO