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
|
// Send notification when a turn completes
|
||||||
if current == "idle" {
|
if current == "idle" {
|
||||||
m.sendNotification("Ollie", "Session "+sess.id+" finished")
|
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.Interrupt(agent.ErrInterrupted)
|
||||||
sess.core.Close()
|
sess.core.Close()
|
||||||
sess.cancel()
|
sess.cancel()
|
||||||
|
m.removePersistedSession(sessionID)
|
||||||
|
|
||||||
m.conn.Emit(busPath, busIface+".SessionKilled", sessionID)
|
m.conn.Emit(busPath, busIface+".SessionKilled", sessionID)
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
@ -532,6 +534,81 @@ func activeSessionsDir() string {
|
||||||
return filepath.Join(paths.DataDir(), "active-sessions")
|
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.
|
// saveAllSessions persists every active session to disk so they can be restored on restart.
|
||||||
func (m *SessionManager) saveAllSessions() {
|
func (m *SessionManager) saveAllSessions() {
|
||||||
dir := activeSessionsDir()
|
dir := activeSessionsDir()
|
||||||
|
|
@ -668,6 +745,7 @@ func (m *SessionManager) restoreSession(ps *agent.PersistedSession) error {
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
id: sessID,
|
id: sessID,
|
||||||
agent: agentName,
|
agent: agentName,
|
||||||
|
log: rebuildChatLog(ps.Messages),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to events for the chat log + signal dispatch
|
// Subscribe to events for the chat log + signal dispatch
|
||||||
|
|
@ -732,6 +810,7 @@ func (m *SessionManager) restoreSession(ps *agent.PersistedSession) error {
|
||||||
|
|
||||||
if current == "idle" {
|
if current == "idle" {
|
||||||
m.sendNotification("Ollie", "Session "+sess.id+" finished")
|
m.sendNotification("Ollie", "Session "+sess.id+" finished")
|
||||||
|
m.persistSession(sess.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -1110,8 +1189,13 @@ func main() {
|
||||||
|
|
||||||
// Wait for signal
|
// Wait for signal
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
<-sigCh
|
|
||||||
|
// Shutdown on signal or D-Bus disconnection (session bus gone).
|
||||||
|
select {
|
||||||
|
case <-sigCh:
|
||||||
|
case <-conn.Context().Done():
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("ollie-dbus daemon shutting down")
|
fmt.Println("ollie-dbus daemon shutting down")
|
||||||
mgr.Shutdown()
|
mgr.Shutdown()
|
||||||
|
|
|
||||||
Reference in New Issue