]> git.giacomolli.uk - onechat/commitdiff
Keep a built leaf when the view lags the tree
authorAnderson A. Giacomolli <agiacomolli@gmail.com>
Fri, 6 Nov 2026 21:53:34 +0000 (18:53 -0300)
committerAnderson A. Giacomolli <agiacomolli@gmail.com>
Fri, 6 Nov 2026 21:53:34 +0000 (18:53 -0300)
A chat opens again after a crash between the build of a free leaf and
the save of the view. The catch-up adds the messages that the view does
not cover, and it now keeps a leaf that the tree holds already instead
of a second build of it, which the tree refuses as a duplicate. The
parent of such a leaf is settled at the start, with the other built
nodes.

A process that is killed during writes then opens again, so the log
loses no message that it acknowledged.

Issue: 5daf23e

memory.go
memory_test.go

index cb55cab9820de7f5cda2436921312c1d1cd31f56..c3e9f454177837e6352f35b098a1dc53a0d73ad9 100644 (file)
--- a/memory.go
+++ b/memory.go
@@ -272,15 +272,24 @@ func (m *Memory) Append(kind, text string) (Message, error) {
 }
 
 // addMessage builds the leaf of message when it needs no model call, or else
 }
 
 // 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.
+// 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 {
 func (m *Memory) addMessage(message Message) error {
+       built, err := m.tree.Built(0, message.ID)
+       if err != nil {
+               return err
+       }
+
        leaf, free := freeLeaf(message)
        leaf, free := freeLeaf(message)
-       if free {
-               err := m.build(leaf)
+       switch {
+       case built:
+       case free:
+               err = m.build(leaf)
                if err != nil {
                        return err
                }
                if err != nil {
                        return err
                }
-       } else {
+       default:
                m.queue.AddLeaf(message.ID)
        }
 
                m.queue.AddLeaf(message.ID)
        }
 
index 8c34149cb07b18997df2d1a950e93e6c41f0fb34..7b3979503a012f666acc99d887ed792fd16bd485 100644 (file)
@@ -54,6 +54,33 @@ func TestMemoryRestartLoadsTheSameView(t *testing.T) {
        require.Equal(t, saved, loaded)
 }
 
        require.Equal(t, saved, loaded)
 }
 
+func TestMemoryOpensAfterACrashBeforeTheViewSave(t *testing.T) {
+       root := t.TempDir()
+       clock := newTestClock()
+       memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
+       require.NoError(t, err)
+
+       _, err = memory.Append("user", "first")
+       require.NoError(t, err)
+
+       saved, err := os.ReadFile(filepath.Join(root, "view.json"))
+       require.NoError(t, err)
+
+       _, err = memory.Append("user", "second")
+       require.NoError(t, err)
+
+       view := memory.View()
+       require.NoError(t, memory.Close())
+
+       // The log and the tree hold message 1, but the view does not.
+       require.NoError(t, os.WriteFile(filepath.Join(root, "view.json"), saved, 0o600))
+
+       memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
+       require.NoError(t, err)
+       require.Equal(t, view, memory.View())
+       require.NoError(t, memory.Close())
+}
+
 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
        memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
        require.NoError(t, err)
 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
        memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
        require.NoError(t, err)