]> git.giacomolli.uk - onechat/blobdiff - memory.go
Add the compaction view
[onechat] / memory.go
index 0be2fde9f025ef2732f56f8c9c8786d00a262e5e..eb28341f69821e6158ad49e29fd2b644314ecff3 100644 (file)
--- a/memory.go
+++ b/memory.go
@@ -14,21 +14,27 @@ import (
 // errNoView tells that a chat has messages but no saved view.
 var errNoView = errors.New("the chat has messages but no saved view")
 
-// Memory is the memory of one chat: the message log, the summary tree, and the
-// view. It saves the view to view.json after each change and loads it at start.
-// It never builds the view again from the log, because a new view differs from
-// the live one and so loses the prompt cache.
+// Memory is the memory of one chat: the message log, the summary tree, the chat
+// view, and the compaction view. The compaction view is the chat view merged
+// further, at the limits of 16 to 32 KB. It takes the same new lines, and it
+// merges again in one batch when it is too large or when the chat view merges.
+// So the compactions share its prefix in the prompt cache. It saves the views
+// to view.json after each change and loads them at start. It never builds the
+// views again from the log, because a new view differs from the live one and so
+// loses the prompt cache.
 type Memory struct {
        mutex sync.Mutex
        root  string
        log   *Log
        tree  *Tree
        chat  *View
+       cview *View
 }
 
 // viewFile is the content of view.json.
 type viewFile struct {
-       Chat viewState `json:"chat"`
+       Chat       viewState `json:"chat"`
+       Compaction viewState `json:"compaction"`
 }
 
 // openMemory opens the chat in root. The clock gives the dates of new messages
@@ -64,6 +70,8 @@ func (m *Memory) load() error {
        data, err := os.ReadFile(m.viewPath())
        if errors.Is(err, os.ErrNotExist) && m.log.Len() == 0 {
                m.chat = newView(chatHigh, chatLow)
+               m.cview = newView(compactionHigh, compactionLow)
+
                return nil
        }
 
@@ -83,7 +91,16 @@ func (m *Memory) load() error {
 
        m.chat, err = restoreView(file.Chat, chatHigh, chatLow, m.tree)
        if err != nil {
-               return err
+               return fmt.Errorf("chat view: %w", err)
+       }
+
+       m.cview, err = restoreView(file.Compaction, compactionHigh, compactionLow, m.tree)
+       if err != nil {
+               return fmt.Errorf("compaction view: %w", err)
+       }
+
+       if m.cview.end != m.chat.end {
+               return fmt.Errorf("compaction view covers %d messages, but the chat view %d", m.cview.end, m.chat.end)
        }
 
        if m.chat.end > m.log.Len() {
@@ -152,7 +169,11 @@ func (m *Memory) addMessage(message Message) error {
                }
        }
 
-       m.chat.Append(m.tree)
+       merged := m.chat.Append(m.tree)
+       m.cview.Append(m.tree)
+       if merged {
+               m.cview.Coarsen(m.chat, m.tree)
+       }
 
        return nil
 }
@@ -188,7 +209,8 @@ func (m *Memory) build(node Node) error {
 // save writes the views to view.json.
 func (m *Memory) save() error {
        data, err := json.Marshal(viewFile{
-               Chat: m.chat.State(),
+               Chat:       m.chat.State(),
+               Compaction: m.cview.State(),
        })
        if err != nil {
                return fmt.Errorf("encode view: %w", err)
@@ -214,6 +236,25 @@ func (m *Memory) View() string {
        return m.chat.Render(m.tree)
 }
 
+// Context returns the compaction view for the build of node (l, i). It ends
+// before the message of a leaf, or after the last message of a merge, and it
+// stops at the first line that is not built.
+func (m *Memory) Context(level, index int) string {
+       m.mutex.Lock()
+       defer m.mutex.Unlock()
+
+       return m.context(level, index)
+}
+
+func (m *Memory) context(level, index int) string {
+       end := index
+       if level > 0 {
+               end = nodeEnd(level, index)
+       }
+
+       return m.cview.Context(m.tree, end)
+}
+
 // writeFileAtomic replaces the file at path with data. A reader or a crash sees
 // the old file or the new file, never a part of one.
 func writeFileAtomic(path string, data []byte) error {