X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/9f17bfb1173524553fff5301b0664267b430e6e3..e5343bd4c3ec69192e92f4be339b855eef36b6ec:/memory.go diff --git a/memory.go b/memory.go index 0be2fde..eb28341 100644 --- 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 {