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
This commit is contained in:
parent
bb2ced28d8
commit
99a8dbfe88
88
main.go
88
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()
|
||||
|
|
|
|||
Reference in New Issue