X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/baf7a940be8927278e3b4b87e7366cebfc16532f..0979625154094df73577d6e2be2d0e9a0a87306c:/memory.go diff --git a/memory.go b/memory.go index 9846c98..49215ac 100644 --- a/memory.go +++ b/memory.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -12,60 +13,14 @@ import ( "time" ) -// errMemoryNoView tells that a chat has messages but no saved view. -var errMemoryNoView = errors.New("the chat has messages but no saved view") +var ( + // errMemoryNoView tells that a chat has messages but no saved view. + errMemoryNoView = errors.New("the chat has messages but no saved view") -// 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. The builds -// stop when ctx ends or at Close. -func openMemory(ctx context.Context, root string, clock func() time.Time, summarizer Summarizer) (*Memory, error) { - log, err := openLog(ctx, root, clock) - if err != nil { - return nil, err - } - - m := &Memory{ - root: root, - log: log, - queue: newBuildQueue(), - summarizer: summarizer, - changed: make(chan struct{}), - } - - m.ctx, m.cancel = context.WithCancel(ctx) - - m.syncer, err = openSyncer(root) - if err != nil { - return nil, errors.Join(err, m.Close()) - } - - m.tree, err = openTree(ctx, root, log.Len(), clock) - if err != nil { - return nil, errors.Join(err, m.Close()) - } - - err = m.load() - if err != nil { - return nil, errors.Join(fmt.Errorf("load view: %w", err), m.Close()) - } - - err = m.restoreQueue() - if err != nil { - return nil, errors.Join(fmt.Errorf("restore build queue: %w", err), m.Close()) - } - - err = m.catchUp() - if err != nil { - return nil, errors.Join(err, m.Close()) - } - - m.mutex.Lock() - m.syncer.Written() - m.dispatch() - m.mutex.Unlock() - - return m, nil -} + // errMemoryUnsettled tells that a line of the view stays pending, + // because its build failed. + errMemoryUnsettled = errors.New("a summary of the view failed, and it is tried again at the next message") +) // Summarizer builds the text of one node with a model. type Summarizer interface { @@ -100,137 +55,104 @@ type Memory struct { cview *View queue *BuildQueue summarizer Summarizer + logger *slog.Logger ctx context.Context cancel context.CancelFunc workers sync.WaitGroup changed chan struct{} } -// Close stops the builds, waits for them, makes the writes durable, closes the -// files of the chat, and releases its lock. -func (m *Memory) Close() error { - m.cancel() - m.workers.Wait() +// 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. Images holds the leaf +// image filenames and is empty for merges. +type Task struct { + Level int + Index int + Context string + Input []string + Images []string +} - m.mutex.Lock() - defer m.mutex.Unlock() +// MemoryViewFile is the content of view.json. +type MemoryViewFile struct { + Chat ViewState `json:"chat"` + Compaction ViewState `json:"compaction"` +} - var err error - if m.syncer != nil { - err = m.syncer.Close() +// 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 *slog.Logger) (*Memory, error) { + log, err := openLog(ctx, root, clock) + if err != nil { + return nil, err } - if m.tree != nil { - err = errors.Join(err, m.tree.Close()) + m := &Memory{ + root: root, + log: log, + queue: newBuildQueue(), + summarizer: summarizer, + logger: logger, + changed: make(chan struct{}), } - return errors.Join(err, m.log.Close()) -} - -// Append logs a new message, adds it to the tree and the views, and starts the -// builds that are ready, also the failed ones. -func (m *Memory) Append(kind, text string) (Message, error) { - m.mutex.Lock() - defer m.mutex.Unlock() + m.ctx, m.cancel = context.WithCancel(ctx) - message, err := m.log.Append(kind, text) + m.syncer, err = openSyncer(root) if err != nil { - return Message{}, err + return nil, errors.Join(err, m.Close()) } - defer m.syncer.Written() - - err = m.addMessage(message) + m.tree, err = openTree(ctx, root, log.Len(), clock) if err != nil { - return Message{}, err + return nil, errors.Join(err, m.Close()) } - err = m.save() + err = m.load() if err != nil { - return Message{}, err + return nil, errors.Join(fmt.Errorf("load view: %w", err), m.Close()) } - m.queue.Retry() - m.dispatch() - - 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 - for n := range children { - child, built, err := m.tree.Node(key.level-1, 2*key.index+n) - if err != nil { - return Node{}, Node{}, err - } - - if !built { - return Node{}, Node{}, fmt.Errorf("node (%d, %d) is not built", key.level-1, 2*key.index+n) - } - - children[n] = child + err = m.restoreQueue() + if err != nil { + return nil, errors.Join(fmt.Errorf("restore build queue: %w", err), m.Close()) } - return children[0], children[1], nil -} - -// Wait returns when no build runs. The failed builds wait for the next message. -func (m *Memory) Wait(ctx context.Context) error { - for { - m.mutex.Lock() - running := m.queue.Running() - changed := m.changed - m.mutex.Unlock() - - if running == 0 { - return nil - } - - select { - case <-changed: - case <-ctx.Done(): - return ctx.Err() - } + err = m.catchUp() + if err != nil { + return nil, errors.Join(err, m.Close()) } -} - -// Flush returns when every write before the call is durable. -func (m *Memory) Flush(ctx context.Context) error { - return m.syncer.Flush(ctx) -} -// 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 { m.mutex.Lock() - defer m.mutex.Unlock() + m.syncer.Written() + m.dispatch() + m.mutex.Unlock() - return m.chat.LinesWithout(skip) + return m, nil } -// Len returns the number of messages in the log. -func (m *Memory) Len() int { - m.mutex.Lock() - defer m.mutex.Unlock() - - return m.log.Len() -} +// Close stops the builds, waits for them, makes the writes durable, closes the +// files of the chat, and releases its lock. +func (m *Memory) Close() error { + m.cancel() + m.workers.Wait() -// Message reads the message with the given ID. -func (m *Memory) Message(id int) (Message, error) { m.mutex.Lock() defer m.mutex.Unlock() - return m.log.Message(id) -} + var err error -// Node reads the node (l, i) if it is built. -func (m *Memory) Node(level, index int) (Node, bool, error) { - m.mutex.Lock() - defer m.mutex.Unlock() + if m.syncer != nil { + err = m.syncer.Close() + } - return m.tree.Node(level, index) + if m.tree != nil { + err = errors.Join(err, m.tree.Close()) + } + + return errors.Join(err, m.log.Close()) } // load reads the saved views. @@ -252,6 +174,7 @@ func (m *Memory) load() error { } var file MemoryViewFile + err = json.Unmarshal(data, &file) if err != nil { return fmt.Errorf("decode view: %w", err) @@ -299,6 +222,7 @@ func (m *Memory) restoreQueue() error { leaf, free := freeLeaf(message) if !free { m.queue.AddLeaf(id) + continue } @@ -360,19 +284,72 @@ func (m *Memory) dispatch() { task, err := m.task(key) if err != nil { + m.failed(key, fmt.Errorf("read its input: %w", err)) m.queue.Fail(key) + continue } m.workers.Add(1) + go m.run(key, task) } } +func (m *Memory) viewPath() string { + return filepath.Join(m.root, "view.json") +} + +// build adds node to the tree, keeps it in the views when it is a pending line, +// and settles its parent. +func (m *Memory) build(node Node) error { + err := m.tree.Add(node) + if err != nil { + return err + } + + m.chat.Refresh(node) + m.cview.Refresh(node) + + return m.settle(NodeKey{node.Level, node.Index}) +} + +// settle looks at the parent of the built node key when both children are +// built. It builds each parent that needs no model call, up the tree, and adds +// the first other parent to the queue. +func (m *Memory) settle(key NodeKey) error { + for { + parent := NodeKey{key.level + 1, key.index / 2} + ready, err := m.ready(parent) + if err != nil || !ready { + return err + } + + left, right, err := m.children(parent) + if err != nil { + return err + } + + node, free := freeMerge(left, right) + if !free { + m.queue.AddMerge(parent) + + return nil + } + + err = m.tree.Add(node) + if err != nil { + return err + } + + key = parent + } +} + // addMessage builds the leaf of message when it needs no model call, or else -// adds the leaf to the queue. Then it appends the leaf to the views. A crash can -// stop an append after the leaf build and before the view save, so a leaf that -// is built already stays as it is, and restoreQueue settles its parent. +// adds the leaf to the queue. Then it appends the leaf to the views. A crash +// can stop an append after the leaf build and before the view save, so a leaf +// that is built already stays as it is, and restoreQueue settles its parent. func (m *Memory) addMessage(message Message) error { built, err := m.tree.Built(0, message.ID) if err != nil { @@ -426,55 +403,6 @@ func (m *Memory) save() error { return nil } -func (m *Memory) viewPath() string { - return filepath.Join(m.root, "view.json") -} - -// build adds node to the tree, keeps it in the views when it is a pending line, -// and settles its parent. -func (m *Memory) build(node Node) error { - err := m.tree.Add(node) - if err != nil { - return err - } - - m.chat.Refresh(node) - m.cview.Refresh(node) - - return m.settle(NodeKey{node.Level, node.Index}) -} - -// settle looks at the parent of the built node key when both children are -// built. It builds each parent that needs no model call, up the tree, and adds -// the first other parent to the queue. -func (m *Memory) settle(key NodeKey) error { - for { - parent := NodeKey{key.level + 1, key.index / 2} - ready, err := m.ready(parent) - if err != nil || !ready { - return err - } - - left, right, err := m.children(parent) - if err != nil { - return err - } - - node, free := freeMerge(left, right) - if !free { - m.queue.AddMerge(parent) - return nil - } - - err = m.tree.Add(node) - if err != nil { - return err - } - - key = parent - } -} - // task returns the build task of the node key, with its input read from the log // or the tree. func (m *Memory) task(key NodeKey) (Task, error) { @@ -491,6 +419,7 @@ func (m *Memory) task(key NodeKey) (Task, error) { } task.Input = []string{leafSource(message)} + task.Images = imageFiles(message.Text) return task, nil } @@ -507,6 +436,11 @@ func (m *Memory) task(key NodeKey) (Task, error) { return task, nil } +// failed logs the failed build of the node key with its cause err. +func (m *Memory) failed(key NodeKey, err error) { + m.logger.Warn("summary failed", "level", key.level, "index", key.index, "err", err) +} + // run builds one node with the summarizer. A failed build stays in the queue // until the next message. func (m *Memory) run(key NodeKey, task Task) { @@ -524,9 +458,14 @@ func (m *Memory) run(key NodeKey, task Task) { Text: text, Size: len(text), }) + m.syncer.Written() } + if err != nil && m.ctx.Err() == nil { + m.failed(key, err) + } + if err != nil { m.queue.Fail(key) } else { @@ -534,6 +473,7 @@ func (m *Memory) run(key NodeKey, task Task) { } close(m.changed) + m.changed = make(chan struct{}) m.dispatch() } @@ -552,6 +492,26 @@ func (m *Memory) ready(key NodeKey) (bool, error) { return !built, err } +// children reads the two children of the node key. +func (m *Memory) children(key NodeKey) (Node, Node, error) { + var children [2]Node + + for n := range children { + child, built, err := m.tree.Node(key.level-1, 2*key.index+n) + if err != nil { + return Node{}, Node{}, err + } + + if !built { + return Node{}, Node{}, fmt.Errorf("node (%d, %d) is not built", key.level-1, 2*key.index+n) + } + + children[n] = child + } + + return children[0], children[1], nil +} + func (m *Memory) context(level, index int) string { end := index if level > 0 { @@ -561,6 +521,192 @@ func (m *Memory) context(level, index int) string { return m.cview.Context(end) } +// Append logs a new message, adds it to the tree and the views, and starts the +// builds that are ready, also the failed ones. +func (m *Memory) Append(kind, text string) (Message, error) { + return m.AppendDated(kind, text, "") +} + +// AppendDated appends a message with its RFC 3339 date and builds its nodes. An +// empty date uses the clock, as Append does. +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) + } + + defer m.syncer.Written() + + err = m.addMessage(message) + if err != nil { + return Message{}, fmt.Errorf("add dated message to memory: %w", err) + } + + err = m.save() + if err != nil { + return Message{}, fmt.Errorf("save dated message view: %w", err) + } + + m.queue.Retry() + m.dispatch() + + 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 +} + +// Wait returns when no build runs. The failed builds wait for the next message. +func (m *Memory) Wait(ctx context.Context) error { + for { + m.mutex.Lock() + + running := m.queue.Running() + changed := m.changed + m.mutex.Unlock() + + if running == 0 { + return nil + } + + select { + case <-changed: + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// Settle returns when the chat view has no pending line, so that every message +// so far is summarized. A pending line with no build that runs belongs to a +// failed build, which waits for the next message, so Settle returns +// errMemoryUnsettled in place of a wait that never ends. +func (m *Memory) Settle(ctx context.Context) error { + for { + m.mutex.Lock() + + pending := m.chat.Pending() + running := m.queue.Running() + changed := m.changed + m.mutex.Unlock() + + if !pending { + return nil + } + + if running == 0 { + return errMemoryUnsettled + } + + select { + case <-changed: + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// 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 +} + +// Flush returns when every write before the call is durable. +func (m *Memory) Flush(ctx context.Context) error { + return m.syncer.Flush(ctx) +} + +// 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 { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.chat.LinesWithout(skip) +} + +// Len returns the number of messages in the log. +func (m *Memory) Len() int { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.log.Len() +} + +// Message reads the message with the given ID. +func (m *Memory) Message(id int) (Message, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.log.Message(id) +} + +// Node reads the node (l, i) if it is built. +func (m *Memory) Node(level, index int) (Node, bool, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.tree.Node(level, index) +} + // View returns the chat view as the model reads it. func (m *Memory) View() string { m.mutex.Lock() @@ -579,18 +725,76 @@ func (m *Memory) Context(level, index int) string { return m.context(level, index) } -// 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. -type Task struct { - Level int - Index int - Context string - Input []string +// rebuildComplete checks the whole tree, not only the visible view. A compactor +// can fail without an HTTP error, for example with an empty line. Such a failed +// merge can be absent from the view and must prevent a swap. +func (m *Memory) rebuildComplete() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if len(m.queue.leaves) != 0 || len(m.queue.queued) != 0 { + return errors.New("rebuild failed: summary tree has unfinished nodes") + } + + return nil } -// MemoryViewFile is the content of view.json. -type MemoryViewFile struct { - Chat ViewState `json:"chat"` - Compaction ViewState `json:"compaction"` +// buildModelLines returns the sizes of the nodes that the model wrote, in tree +// order. A node that fits its source with no model call is not one of them. +func (m *Memory) buildModelLines() ([]int, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + keys, err := m.tree.Keys() + if err != nil { + return nil, fmt.Errorf("list built nodes: %w", err) + } + + var sizes []int + + for _, key := range keys { + var node Node + + node, _, err = m.tree.Node(key.level, key.index) + if err != nil { + return nil, fmt.Errorf("read built node: %w", err) + } + + var model bool + + model, err = m.buildBuiltByModel(key) + if err != nil { + return nil, fmt.Errorf("check model source: %w", err) + } + + if model { + sizes = append(sizes, node.Size) + } + } + + return sizes, nil +} + +// buildBuiltByModel tells if the node key needed a model call: a leaf whose +// source is over nodeBytes, or a merge whose children do not fit in one node. +func (m *Memory) buildBuiltByModel(key NodeKey) (bool, error) { + if key.level == 0 { + message, err := m.log.Message(key.index) + if err != nil { + return false, fmt.Errorf("read leaf source: %w", err) + } + + _, free := freeLeaf(message) + + return !free, nil + } + + left, right, err := m.children(key) + if err != nil { + return false, fmt.Errorf("read merge sources: %w", err) + } + + _, free := freeMerge(left, right) + + return !free, nil }