"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")
+
+ // 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")
+)
// 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
}
}
+// 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()
+ }
+ }
+}
+
// Flush returns when every write before the call is durable.
func (m *Memory) Flush(ctx context.Context) error {
return m.syncer.Flush(ctx)
package main
import (
+ "context"
"fmt"
"maps"
"os"
}
}
+func TestMemorySettleWaitsForThePendingLines(t *testing.T) {
+ summarizer := newFakeSummarizer()
+ summarizer.gate = make(chan struct{})
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ _, err = memory.Append("echo", memoryLongText(0))
+ require.NoError(t, err)
+ require.Contains(t, memory.View(), viewPending)
+
+ settled := make(chan error, 1)
+ go func() {
+ settled <- memory.Settle(t.Context())
+ }()
+
+ select {
+ case err = <-settled:
+ require.Fail(t, "Settle returned before the build", "error: %v", err)
+ case <-time.After(50 * time.Millisecond):
+ }
+
+ close(summarizer.gate)
+ require.NoError(t, <-settled)
+ require.NotContains(t, memory.View(), viewPending)
+}
+
+func TestMemorySettleFailsWhileABuildFailed(t *testing.T) {
+ summarizer := newFakeSummarizer()
+ summarizer.SetFailing(true)
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ _, err = memory.Append("echo", memoryLongText(0))
+ require.NoError(t, err)
+ require.ErrorIs(t, memory.Settle(t.Context()), errMemoryUnsettled)
+
+ summarizer.SetFailing(false)
+ _, err = memory.Append("user", "retry")
+ require.NoError(t, err)
+ require.NoError(t, memory.Settle(t.Context()))
+}
+
+func TestMemorySettleEndsWithItsContext(t *testing.T) {
+ summarizer := newFakeSummarizer()
+ summarizer.gate = make(chan struct{})
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ _, err = memory.Append("echo", memoryLongText(0))
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
+ defer cancel()
+
+ require.ErrorIs(t, memory.Settle(ctx), context.DeadlineExceeded)
+}
+
func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
summarizer := newFakeSummarizer()
summarizer.SetFailing(true)