From 99a8dbfe88bbbb43da8eeab9fcadb52b44c6113a Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:58:06 +0200 Subject: [PATCH] session persistence: SIGHUP, bus disconnect, checkpointing, chat log restore - Add SIGHUP to signal handler for non-systemd session teardown - Detect D-Bus session bus disconnection via conn.Context().Done() - Checkpoint individual sessions to disk on every idle transition - Remove checkpoint files when sessions are killed - Rebuild visible chat log from last 8 messages on restore --- main.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 7a86f53..b3b59ed 100644 --- a/main.go +++ b/main.go @@ -197,6 +197,7 @@ func (m *SessionManager) CreateSession(cwd, backendName, modelName, agentName st // Send notification when a turn completes if current == "idle" { m.sendNotification("Ollie", "Session "+sess.id+" finished") + m.persistSession(sess.id) } } }() @@ -234,6 +235,7 @@ func (m *SessionManager) KillSession(sessionID string) (bool, *dbus.Error) { sess.core.Interrupt(agent.ErrInterrupted) sess.core.Close() sess.cancel() + m.removePersistedSession(sessionID) m.conn.Emit(busPath, busIface+".SessionKilled", sessionID) return true, nil @@ -532,6 +534,81 @@ func activeSessionsDir() string { return filepath.Join(paths.DataDir(), "active-sessions") } +// persistSession saves a single session to the active-sessions directory. +// Called on every idle transition for crash resilience. +func (m *SessionManager) persistSession(id string) { + if strings.HasSuffix(id, "-copilot") { + return + } + dir := activeSessionsDir() + os.MkdirAll(dir, 0700) + + m.mu.RLock() + sess, ok := m.sessions[id] + m.mu.RUnlock() + if !ok { + return + } + + path := filepath.Join(dir, id+".json") + if err := sess.core.SaveSession(path); err != nil { + fmt.Fprintf(os.Stderr, "checkpoint session %s: %v\n", id, err) + } +} + +// removePersistedSession deletes the checkpoint file for a killed session. +func (m *SessionManager) removePersistedSession(id string) { + path := filepath.Join(activeSessionsDir(), id+".json") + os.Remove(path) +} + +// rebuildChatLog reconstructs a human-readable chat log from the tail of the +// persisted message list. This gives restored sessions visible history. +func rebuildChatLog(messages []backend.Message) []byte { + // Show up to the last hotTailSize messages (matches the verbatim context window). + const tailSize = 8 + start := 0 + if len(messages) > tailSize { + start = len(messages) - tailSize + } + tail := messages[start:] + + var buf strings.Builder + if start > 0 { + buf.WriteString(fmt.Sprintf("[info] session restored (%d earlier messages compacted)\n\n", start)) + } + for _, msg := range tail { + switch msg.Role { + case "system": + continue // don't show system messages in chat log + case "user": + buf.WriteString("[user]\n" + msg.Content + "\n\n") + case "assistant": + if msg.Reasoning != "" { + buf.WriteString("[reasoning]\n" + msg.Reasoning + "\n\n") + } + if msg.Content != "" { + buf.WriteString("[assistant]\n" + msg.Content + "\n\n") + } + for _, tc := range msg.ToolCalls { + buf.WriteString("[call:" + tc.Name + "]\n" + string(tc.Arguments) + "\n\n") + } + case "tool": + name := "result" + if msg.ToolCallID != "" { + name = msg.ToolCallID + } + // Truncate long tool results in the log + content := msg.Content + if len(content) > 2000 { + content = content[:2000] + "\n... (truncated)" + } + buf.WriteString("[tool:" + name + "]\n" + content + "\n\n") + } + } + return []byte(buf.String()) +} + // saveAllSessions persists every active session to disk so they can be restored on restart. func (m *SessionManager) saveAllSessions() { dir := activeSessionsDir() @@ -668,6 +745,7 @@ func (m *SessionManager) restoreSession(ps *agent.PersistedSession) error { cancel: cancel, id: sessID, agent: agentName, + log: rebuildChatLog(ps.Messages), } // Subscribe to events for the chat log + signal dispatch @@ -732,6 +810,7 @@ func (m *SessionManager) restoreSession(ps *agent.PersistedSession) error { if current == "idle" { m.sendNotification("Ollie", "Session "+sess.id+" finished") + m.persistSession(sess.id) } } }() @@ -1110,8 +1189,13 @@ func main() { // Wait for signal sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + // Shutdown on signal or D-Bus disconnection (session bus gone). + select { + case <-sigCh: + case <-conn.Context().Done(): + } fmt.Println("ollie-dbus daemon shutting down") mgr.Shutdown()