X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/522d7fd117285e4e4ce3ea5ec9b5a1722521b681..0da50dc187c8b4aa23ac1ddd0418c0da779389d9:/memory.go?ds=sidebyside diff --git a/memory.go b/memory.go index 7cbcce6..602b86c 100644 --- a/memory.go +++ b/memory.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "log" + "log/slog" "os" "path/filepath" "strings" @@ -25,7 +25,7 @@ var ( // openMemory opens the chat in root and starts the builds that are ready. The // clock gives the dates of new messages and the days of new nodes. logger gets // one line for each failed build. The builds stop when ctx ends or at Close. -func openMemory(ctx context.Context, root string, clock func() time.Time, summarizer Summarizer, logger *log.Logger) (*Memory, error) { +func openMemory(ctx context.Context, root string, clock func() time.Time, summarizer Summarizer, logger *slog.Logger) (*Memory, error) { log, err := openLog(ctx, root, clock) if err != nil { return nil, err @@ -108,7 +108,7 @@ type Memory struct { cview *View queue *BuildQueue summarizer Summarizer - logger *log.Logger + logger *slog.Logger ctx context.Context cancel context.CancelFunc workers sync.WaitGroup @@ -149,6 +149,11 @@ func (m *Memory) AppendDated(kind, text, date string) (Message, error) { m.mutex.Lock() defer m.mutex.Unlock() + return m.appendDated(kind, text, date) +} + +// appendDated appends under the memory lock and updates the views and builds. +func (m *Memory) appendDated(kind, text, date string) (Message, error) { message, err := m.log.AppendDated(kind, text, date) if err != nil { return Message{}, fmt.Errorf("append dated log message: %w", err) @@ -172,6 +177,44 @@ func (m *Memory) AppendDated(kind, text, date string) (Message, error) { return message, nil } +// AppendOnce appends a queued message. prepare saves its log ID before the +// append, under the memory lock. A saved ID permits recovery after a stop +// between that save and the append, without a second log entry. prepare must +// not call Memory. The caller stops delivery after a failed append. +func (m *Memory) AppendOnce(kind, text string, id *int, prepare func(int) error) (Message, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if id != nil && *id < m.log.Len() { + message, err := m.log.Message(*id) + if err != nil { + return Message{}, fmt.Errorf("read queued message: %w", err) + } + + if message.Kind != kind || message.Text != text { + return Message{}, errors.New("queued message does not match its log entry") + } + + return message, nil + } + + if id != nil && *id != m.log.Len() { + return Message{}, errors.New("queued message is beyond the log") + } + + err := prepare(m.log.Len()) + if err != nil { + return Message{}, fmt.Errorf("prepare queued message: %w", err) + } + + message, err := m.appendDated(kind, text, "") + if err != nil { + return Message{}, fmt.Errorf("append queued message: %w", err) + } + + return message, nil +} + // children reads the two children of the node key. func (m *Memory) children(key NodeKey) (Node, Node, error) { var children [2]Node @@ -247,6 +290,26 @@ func (m *Memory) Flush(ctx context.Context) error { return m.syncer.Flush(ctx) } +// Snapshot flushes all memory writes and runs snapshot while no append or +// summary write can change the log, indexes, tree, or views. snapshot must not +// call Memory. +func (m *Memory) Snapshot(ctx context.Context, snapshot func() error) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + err := m.Flush(ctx) + if err != nil { + return fmt.Errorf("flush memory snapshot: %w", err) + } + + err = snapshot() + if err != nil { + return fmt.Errorf("copy memory snapshot: %w", err) + } + + return nil +} + // ViewLines returns the lines of the chat view, each with its newline, without // the leaf lines of the messages in skip. func (m *Memory) ViewLines(skip map[int]bool) []string { @@ -544,6 +607,7 @@ func (m *Memory) task(key NodeKey) (Task, error) { } task.Input = []string{leafSource(message)} + task.Images = imageFiles(message.Text) return task, nil } @@ -598,7 +662,7 @@ func (m *Memory) run(key NodeKey, task Task) { // failed logs the failed build of the node key with its cause err. func (m *Memory) failed(key NodeKey, err error) { - m.logger.Printf("summary failed, retry at next message: node (%d, %d): %v", key.level, key.index, err) + m.logger.Warn("summary failed", "level", key.level, "index", key.index, "err", err) } // ready tells if both children of the node key are built and the node is not. @@ -718,12 +782,14 @@ func (m *Memory) buildBuiltByModel(key NodeKey) (bool, error) { // Task is the build of the node (Level, Index). Context is the compaction view // for the build. Input holds the source "kind: text" of a leaf, or the two -// child lines of a merge with their newlines as spaces. +// child lines of a merge with their newlines as spaces. Images holds the leaf +// image filenames and is empty for merges. type Task struct { Level int Index int Context string Input []string + Images []string } // MemoryViewFile is the content of view.json.