diff --git a/internal/acp/agent.go b/internal/acp/agent.go index 7c5ec03a..87b11266 100644 --- a/internal/acp/agent.go +++ b/internal/acp/agent.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" + "os" "strings" "sync" @@ -388,15 +390,21 @@ func (a *Agent) modeState(s *acpSession) *SessionModeState { func (a *Agent) persistTurn(sess *acpSession, user, assistant string) { if a.deps.Store != nil { - _, _ = a.deps.Store.AppendEvent(sess.id, sessions.AppendEventInput{ + if _, err := a.deps.Store.AppendEvent(sess.id, sessions.AppendEventInput{ Type: sessions.EventMessage, Payload: map[string]any{"role": "user", "content": user}, - }) + }); err != nil { + // best-effort; log at least so we notice history loss + // (real fix would surface to transcript / user) + fmt.Fprintf(os.Stderr, "warning: failed to persist user turn: %v\n", err) + } if assistant != "" { - _, _ = a.deps.Store.AppendEvent(sess.id, sessions.AppendEventInput{ + if _, err := a.deps.Store.AppendEvent(sess.id, sessions.AppendEventInput{ Type: sessions.EventMessage, Payload: map[string]any{"role": "assistant", "content": assistant}, - }) + }); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to persist assistant turn: %v\n", err) + } } } sess.appendHistory(turnRecord{user: user, assistant: assistant}) diff --git a/internal/acp/agent_test.go b/internal/acp/agent_test.go index 9b23fe2d..23bf2e50 100644 --- a/internal/acp/agent_test.go +++ b/internal/acp/agent_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "os" "strings" "testing" "time" @@ -165,6 +166,41 @@ func TestACPUnknownSessionPromptErrors(t *testing.T) { } } +// TestPersistTurnLogsAppendEventFailures proves that a persistence failure +// (previously silently discarded via `_, _ = ...AppendEvent(...)`) now gets +// logged instead of vanishing without a trace. An invalid session id makes +// AppendEvent fail deterministically without needing to break the store. +func TestPersistTurnLogsAppendEventFailures(t *testing.T) { + store := sessions.NewStore(sessions.StoreOptions{RootDir: t.TempDir()}) + a := &Agent{deps: Deps{Store: store}} + sess := &acpSession{id: "not a valid session id!!"} + + origStderr := os.Stderr + read, write, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe: %v", err) + } + os.Stderr = write + + a.persistTurn(sess, "hello", "world") + + if err := write.Close(); err != nil { + t.Fatalf("close pipe writer: %v", err) + } + os.Stderr = origStderr + + captured, err := io.ReadAll(read) + if err != nil { + t.Fatalf("read captured stderr: %v", err) + } + output := string(captured) + for _, want := range []string{"failed to persist user turn", "failed to persist assistant turn"} { + if !strings.Contains(output, want) { + t.Fatalf("expected stderr to contain %q, got %q", want, output) + } + } +} + func TestACPSetModeUpdatesSession(t *testing.T) { h := newHarness(t, testDeps(t)) defer h.stop()