14 // errNoView tells that a chat has messages but no saved view.
15 var errNoView = errors.New("the chat has messages but no saved view")
17 // Memory is the memory of one chat: the message log, the summary tree, and the
18 // view. It saves the view to view.json after each change and loads it at start.
19 // It never builds the view again from the log, because a new view differs from
20 // the live one and so loses the prompt cache.
29 // viewFile is the content of view.json.
30 type viewFile struct {
31 Chat viewState `json:"chat"`
34 // openMemory opens the chat in root. The clock gives the dates of new messages
35 // and the days of new nodes.
36 func openMemory(ctx context.Context, root string, clock func() time.Time) (*Memory, error) {
37 log, err := openLog(ctx, root, clock)
47 m.tree, err = openTree(ctx, root, log.Len(), clock)
49 return nil, errors.Join(err, m.Close())
54 return nil, errors.Join(fmt.Errorf("load view: %w", err), m.Close())
60 // load reads the saved view. A crash can stop an append after the log write and
61 // before the view save, so load continues the view with the messages that it
62 // does not cover yet.
63 func (m *Memory) load() error {
64 data, err := os.ReadFile(m.viewPath())
65 if errors.Is(err, os.ErrNotExist) && m.log.Len() == 0 {
66 m.chat = newView(chatHigh, chatLow)
70 if errors.Is(err, os.ErrNotExist) {
75 return fmt.Errorf("read view: %w", err)
79 err = json.Unmarshal(data, &file)
81 return fmt.Errorf("decode view: %w", err)
84 m.chat, err = restoreView(file.Chat, chatHigh, chatLow, m.tree)
89 if m.chat.end > m.log.Len() {
90 return fmt.Errorf("view covers %d messages, but the log has %d", m.chat.end, m.log.Len())
93 if m.chat.end == m.log.Len() {
97 for m.chat.end < m.log.Len() {
98 err = m.addMessage(m.log.Message(m.chat.end))
107 // Close closes the files of the chat and releases its lock.
108 func (m *Memory) Close() error {
110 defer m.mutex.Unlock()
117 return errors.Join(err, m.log.Close())
120 // Append logs a new message and adds it to the tree and the view.
121 func (m *Memory) Append(kind, text string) (Message, error) {
123 defer m.mutex.Unlock()
125 message, err := m.log.Append(kind, text)
127 return Message{}, err
130 err = m.addMessage(message)
132 return Message{}, err
137 return Message{}, err
143 // addMessage builds the leaf of message when it needs no model call and appends
145 func (m *Memory) addMessage(message Message) error {
146 _, built := m.tree.Node(0, message.ID)
147 leaf, free := freeLeaf(message)
155 m.chat.Append(m.tree)
160 // build adds node to the tree, and then each parent above it that needs no
162 func (m *Memory) build(node Node) error {
164 err := m.tree.Add(node)
169 sibling, built := m.tree.Node(node.Level, node.Index^1)
174 left, right := node, sibling
175 if node.Index%2 != 0 {
176 left, right = sibling, node
179 parent, free := freeMerge(left, right)
188 // save writes the views to view.json.
189 func (m *Memory) save() error {
190 data, err := json.Marshal(viewFile{
191 Chat: m.chat.State(),
194 return fmt.Errorf("encode view: %w", err)
197 err = writeFileAtomic(m.viewPath(), append(data, '\n'))
199 return fmt.Errorf("save view: %w", err)
205 func (m *Memory) viewPath() string {
206 return filepath.Join(m.root, "view.json")
209 // View returns the chat view as the model reads it.
210 func (m *Memory) View() string {
212 defer m.mutex.Unlock()
214 return m.chat.Render(m.tree)
217 // writeFileAtomic replaces the file at path with data. A reader or a crash sees
218 // the old file or the new file, never a part of one.
219 func writeFileAtomic(path string, data []byte) error {
220 dir := filepath.Dir(path)
221 file, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*")
223 return fmt.Errorf("create temporary file: %w", err)
226 _, err = file.Write(data)
231 err = errors.Join(err, file.Close())
233 err = os.Rename(file.Name(), path)
237 return errors.Join(fmt.Errorf("replace file: %w", err), os.Remove(file.Name()))
243 // syncDir flushes the entries of dir to the disk.
244 func syncDir(dir string) (err error) {
245 handle, err := os.Open(dir)
247 return fmt.Errorf("open directory: %w", err)
251 err = errors.Join(err, handle.Close())
256 return fmt.Errorf("flush directory: %w", err)