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.
This commit is contained in:
patriceckhart 2026-04-20 12:11:15 +02:00
parent 1dafef8fea
commit 616eed3bd6

View file

@ -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