From 616eed3bd63b2b12fdca5d1858c8ffe98a565fea Mon Sep 17 00:00:00 2001 From: patriceckhart Date: Mon, 20 Apr 2026 12:11:15 +0200 Subject: [PATCH] fix(tui): persist transcript to session file after every turn Previously the tui lazily flushed the agent messages to the session file only at exit via WriteNewTranscript, plus opt-in via /session export or /session tree. That meant a mid-session crash, kill -9, or power loss dropped the entire conversation from disk even though the summary was visible in the scrollback. Now the turn-drain goroutine in startTurn() calls FlushSession() right after i.agent.Prompt returns, while the turn memory is still hot. FlushSession is the same idempotent helper used by /session export and /session tree: it appends only the rows past the current baseline and advances the baseline, so double writes cant happen even if the exit-time flush also fires. Ordering in the goroutine: lock -> clear busy/streamOn/cancel -> read the flush callback -> unlock -> flush -> relock for the queue-drain and auto-compact decisions. The short unlocked window is safe because no other goroutine reads those fields at that moment (busy is already false). No new config hook; reuses the existing FlushSession the cli wires in. --- internal/agent/modes/interactive.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/agent/modes/interactive.go b/internal/agent/modes/interactive.go index 5037677..6857949 100644 --- a/internal/agent/modes/interactive.go +++ b/internal/agent/modes/interactive.go @@ -2068,6 +2068,18 @@ func (i *Interactive) startTurn(parent context.Context, prompt string) { if err != nil && ctx.Err() == nil { i.statusErr = err.Error() } + // Persist the assistant's reply (and every tool row before + // it) to the session file while the turn memory is hot. + // Without this, WriteNewTranscript only fires at zot exit, + // meaning a crash or ungraceful kill drops the whole + // conversation. FlushSession is idempotent (it advances the + // baseline so subsequent flushes only write new rows). + flush := i.cfg.FlushSession + i.mu.Unlock() + if flush != nil { + flush() + } + i.mu.Lock() // Pop the next queued message, if any, and relaunch. var next string var hasNext bool