From dc83763c24693751a7f25219e39a5336a47f8bd8 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sun, 26 Apr 2026 19:24:28 +0200 Subject: [PATCH] agent: save session after each tool round for mid-turn crash recovery Adds SaveSession callback to agentConfig, called after every state.update() in the run loop. On restart, the session JSON contains all completed tool rounds; the model continues naturally from current history. SIGTERM is already handled in olliesrv via InterruptAll + Shutdown, which cancels all session contexts and lets the loop exit cleanly after the current state.update(). Worst-case (SIGKILL) loses one tool round, not the whole turn. Co-Authored-By: Claude Sonnet 4.6 --- pkg/agent/core.go | 1 + pkg/agent/loop.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 9b986d5..b421f42 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -777,6 +777,7 @@ func (s *agent) executeTurn(ctx context.Context, input string, handler EventHand "result": result, }, s.log) } + s.cfg.SaveSession = func() { s.saveSession() } s.cfg.AutoCompact = func(ctx context.Context) { if ctx.Err() != nil || s.session == nil { return diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index ac2f3ef..f344a8e 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -24,6 +24,7 @@ type agentConfig struct { GenerationParams backend.GenerationParams PopInject func() string // returns and clears pending inject, or "" AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place + SaveSession func() // called after each state.update(); persists mid-turn progress PreTool func(ctx context.Context, name string, args json.RawMessage) HookResult // called before each tool; exit 2 blocks execution PostTool func(ctx context.Context, name string, args json.RawMessage, result string) HookResult // called after each tool; exit 0 appends, exit 2 replaces result } @@ -127,6 +128,9 @@ func run(ctx context.Context, cfg agentConfig, state state) error { }) } state.update(msg, results) + if cfg.SaveSession != nil { + cfg.SaveSession() + } return fmt.Errorf("step %d: stream interrupted: %w", step, ctx.Err()) } @@ -252,6 +256,9 @@ func run(ctx context.Context, cfg agentConfig, state state) error { } state.update(msg, results) + if cfg.SaveSession != nil { + cfg.SaveSession() + } if interrupted { return ctx.Err()