18 // nodeBytes is the size limit of a node text in UTF-8 bytes. A model can go a
19 // few bytes over it, because the view measures the real sizes.
22 // treeRecordBytes is the size of a record of index/tree.idx: the position of
23 // the node line, the node index, and the node level.
24 const treeRecordBytes = 32
26 // Tree holds the built nodes of one chat. It writes one JSON line for each node
27 // to tree/YYYY-MM-DD.jsonl, named for the local day of the build. Each node is
28 // built once and never again. A node of a higher level is built only after both
29 // of its children. After a failed write, the tree refuses all writes until it
30 // is opened again. The caller holds the lock of the chat and serializes all
33 // The tree keeps no node in memory. The index index/tree.idx lists the node
34 // lines in file order, and index/tree-L.idx holds the position of node (L, i)
35 // in record i, with a zero record for a node that is not built. At open the
36 // tree indexes the lines after the last indexed one, so a missing index is
37 // built again from the day files.
39 clock func() time.Time
43 levels map[int]*Records
47 // Node is one line of the summary tree. Node (l, i) covers the 2^l messages
48 // from i*2^l on. A node of level 0 holds one message. A node of a higher level
49 // merges its two children, (l-1, 2i) and (l-1, 2i+1). Size is the UTF-8 byte
54 Text string `json:"text"`
55 Size int `json:"size"`
58 // NodeKey names the node (l, i).
64 // openTree opens the tree of the chat in root and indexes its new lines. A chat
65 // of total messages cannot have a node that covers a later message.
66 func openTree(ctx context.Context, root string, total int, clock func() time.Time) (*Tree, error) {
67 for _, dir := range []string{"tree", "index"} {
68 err := os.MkdirAll(filepath.Join(root, dir), 0o700)
70 return nil, fmt.Errorf("create %s directory: %w", dir, err)
77 dir: filepath.Join(root, "tree"),
79 dir: filepath.Join(root, "index"),
80 levels: make(map[int]*Records),
83 err := t.open(ctx, total)
85 return nil, errors.Join(fmt.Errorf("index tree: %w", err), t.Close())
91 // Close closes the files of the tree.
92 func (t *Tree) Close() error {
93 err := t.journal.Close()
95 err = errors.Join(err, t.order.Close())
99 for level, index := range t.levels {
100 err = errors.Join(err, index.Close())
102 delete(t.levels, level)
108 // Add writes a new node to the tree.
109 func (t *Tree) Add(node Node) error {
110 if t.failure != nil {
111 return fmt.Errorf("tree needs a reopen after a failed write: %w", t.failure)
119 line, err := json.Marshal(node)
121 return fmt.Errorf("encode node: %w", err)
124 place, err := t.journal.Write(t.clock().Format(time.DateOnly), append(line, '\n'))
126 err = t.addIndex(NodeKey{node.Level, node.Index}, place)
131 return fmt.Errorf("write node (%d, %d): %w", node.Level, node.Index, err)
137 // Built tells if the node (l, i) is built.
138 func (t *Tree) Built(level, index int) (bool, error) {
139 _, built, err := t.place(level, index)
143 // Node reads the node (l, i) if it is built.
144 func (t *Tree) Node(level, index int) (Node, bool, error) {
145 place, built, err := t.place(level, index)
146 if err != nil || !built {
147 return Node{}, false, err
150 line, err := t.journal.Read(place)
152 return Node{}, false, fmt.Errorf("read node (%d, %d): %w", level, index, err)
157 err = json.Unmarshal(line, &node)
159 return Node{}, false, fmt.Errorf("decode node (%d, %d): %w", level, index, err)
162 if node.Level != level || node.Index != index {
163 return Node{}, false, fmt.Errorf("index of node (%d, %d) points to node (%d, %d)", level, index, node.Level, node.Index)
166 return node, true, nil
169 // Keys returns the keys of the built nodes, ordered by level and then by index.
170 func (t *Tree) Keys() ([]NodeKey, error) {
171 keys := make([]NodeKey, 0, t.order.count)
172 for n := range t.order.count {
173 record, _, err := t.order.Read(n)
178 key, _ := treeDecodeRecord(record)
179 keys = append(keys, key)
182 slices.SortFunc(keys, func(a, b NodeKey) int {
183 return cmp.Or(cmp.Compare(a.level, b.level), cmp.Compare(a.index, b.index))
189 func (t *Tree) open(ctx context.Context, total int) error {
192 t.order, err = openRecords(filepath.Join(t.dir, "tree.idx"), treeRecordBytes)
198 if t.order.count == 0 {
199 err = t.removeLevels()
204 record, _, err := t.order.Read(t.order.count - 1)
209 // A crash can stop an add between the two index writes, so the
210 // last node gets its level record again.
211 node, last := treeDecodeRecord(record)
212 err = t.writeLevel(node, last)
219 offset: last.offset + uint64(last.length),
223 return t.journal.Scan(ctx, start, func(line []byte, place Position) error {
226 err := json.Unmarshal(line, &node)
228 return fmt.Errorf("decode tree node: %w", err)
236 if nodeEnd(node.Level, node.Index) > total {
237 return fmt.Errorf("node (%d, %d) covers messages after the log", node.Level, node.Index)
240 return t.addIndex(NodeKey{node.Level, node.Index}, place)
244 // check makes sure that node is valid and can join the tree now.
245 func (t *Tree) check(node Node) error {
246 if node.Level < 0 || node.Level >= bits.UintSize-2 || node.Index < 0 {
247 return fmt.Errorf("node (%d, %d) is out of range", node.Level, node.Index)
250 if node.Text == "" || !utf8.ValidString(node.Text) || node.Size != len(node.Text) {
251 return fmt.Errorf("node (%d, %d) has no valid text of its size", node.Level, node.Index)
254 built, err := t.Built(node.Level, node.Index)
260 return fmt.Errorf("node (%d, %d) is already built", node.Level, node.Index)
267 for _, index := range []int{2 * node.Index, 2*node.Index + 1} {
268 built, err = t.Built(node.Level-1, index)
274 return fmt.Errorf("node (%d, %d) has a child that is not built", node.Level, node.Index)
281 // addIndex adds the node line at place to both indexes.
282 func (t *Tree) addIndex(node NodeKey, place Position) error {
283 record := make([]byte, treeRecordBytes)
284 encodePosition(record, place)
285 binary.LittleEndian.PutUint64(record[positionBytes:], uint64(node.index))
286 binary.LittleEndian.PutUint32(record[positionBytes+8:], uint32(node.level))
288 err := t.order.Append(record)
293 return t.writeLevel(node, place)
296 // place returns the position of the node (l, i) if it is built.
297 func (t *Tree) place(level, index int) (Position, bool, error) {
298 if level < 0 || level >= bits.UintSize-2 || index < 0 {
299 return Position{}, false, nil
302 records, err := t.level(level)
304 return Position{}, false, err
307 record, found, err := records.Read(index)
308 if err != nil || !found {
309 return Position{}, false, err
312 place := decodePosition(record)
314 return place, place.length > 0, nil
317 // removeLevels removes the level indexes, which are not valid without the order
319 func (t *Tree) removeLevels() error {
320 paths, err := filepath.Glob(filepath.Join(t.dir, "tree-*.idx"))
322 return fmt.Errorf("list level indexes: %w", err)
325 for _, path := range paths {
326 err = os.Remove(path)
328 return fmt.Errorf("remove level index: %w", err)
335 // writeLevel writes the position of node to its level index.
336 func (t *Tree) writeLevel(node NodeKey, place Position) error {
337 level, err := t.level(node.level)
342 record := make([]byte, positionBytes)
343 encodePosition(record, place)
345 return level.Write(node.index, record)
348 // level returns the index of level, and opens it at first use.
349 func (t *Tree) level(level int) (*Records, error) {
350 index, open := t.levels[level]
355 index, err := openRecords(filepath.Join(t.dir, fmt.Sprintf("tree-%d.idx", level)), positionBytes)
360 t.levels[level] = index
365 // Count returns the number of built nodes.
366 func (t *Tree) Count() int {
370 // leafSource returns the text that the leaf of message holds or summarizes.
371 func leafSource(message Message) string {
372 return message.Kind + ": " + message.Text
375 // freeLeaf returns a text-only leaf when its source fits in a node. Images need
376 // a model description even when their text fits.
377 func freeLeaf(message Message) (Node, bool) {
378 text := leafSource(message)
379 if len(text) > nodeBytes || len(imageFiles(message.Text)) > 0 {
391 // freeMerge returns the parent of two sibling nodes when both texts, joined by
392 // a newline, fit in a node. Such a parent needs no model call.
393 func freeMerge(left, right Node) (Node, bool) {
394 if left.Size+1+right.Size > nodeBytes {
398 text := left.Text + "\n" + right.Text
401 Level: left.Level + 1,
402 Index: left.Index / 2,
408 // nodeEnd returns the ID after the last message of node (l, i).
409 func nodeEnd(level, index int) int {
410 return (index + 1) << level
413 // treeDecodeRecord reads the key and the position of a record of
415 func treeDecodeRecord(record []byte) (NodeKey, Position) {
417 level: int(binary.LittleEndian.Uint32(record[positionBytes+8:])),
418 index: int(binary.LittleEndian.Uint64(record[positionBytes:])),
421 return key, decodePosition(record)