// 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
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
}
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() {
}
}
- 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
}
// 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)
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 {