Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.

Commit 86aeb61

Browse files
committed
fix: Sessions
1 parent 6c1ad8a commit 86aeb61

9 files changed

Lines changed: 296 additions & 326 deletions

File tree

cmd/scriptschnell/main.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,13 +1002,39 @@ func runTUI(cfg *config.Config, providerMgr *provider.Manager, cliOptions *cli.O
10021002
return nil
10031003
}
10041004

1005-
sessionID := selectedItem.GetSessionID()
1006-
if sessionID == "" {
1007-
return fmt.Errorf("invalid session selected")
1008-
}
1009-
10101005
switch action {
1006+
case "save":
1007+
// Save current session
1008+
activeOrch := getActiveOrchestrator()
1009+
if activeOrch == nil {
1010+
return fmt.Errorf("no active session to save")
1011+
}
1012+
1013+
saveName := sessionMenu.GetSaveName()
1014+
if saveName == "" {
1015+
saveName = actor.GenerateSessionName("")
1016+
} else {
1017+
saveName = actor.GenerateSessionName(saveName)
1018+
}
1019+
1020+
// Generate session title if not already present
1021+
if err := activeOrch.GenerateSessionTitle(ctx); err != nil {
1022+
logger.Warn("session save: failed to generate title: %v", err)
1023+
}
1024+
1025+
currentSession := activeOrch.GetSession()
1026+
if err := actor.SaveSessionViaActor(ctx, storageRef, currentSession, saveName); err != nil {
1027+
return fmt.Errorf("failed to save session: %w", err)
1028+
}
1029+
1030+
model.AddSystemMessage(fmt.Sprintf("Session saved as '%s'", saveName))
1031+
10111032
case "load":
1033+
sessionID := selectedItem.GetSessionID()
1034+
if sessionID == "" {
1035+
return fmt.Errorf("invalid session selected")
1036+
}
1037+
10121038
// Load the session using actor
10131039
loadedSession, err := actor.LoadSessionViaActor(ctx, storageRef, cfg.WorkingDir, sessionID)
10141040
if err != nil {
@@ -1041,6 +1067,11 @@ func runTUI(cfg *config.Config, providerMgr *provider.Manager, cliOptions *cli.O
10411067
model.AddSystemMessage(fmt.Sprintf("Loaded session: %s", selectedItem.Title()))
10421068

10431069
case "delete":
1070+
sessionID := selectedItem.GetSessionID()
1071+
if sessionID == "" {
1072+
return fmt.Errorf("invalid session selected")
1073+
}
1074+
10441075
// Delete the session using actor
10451076
deleteMsg := actor.SessionStorageDeleteMsg{
10461077
WorkingDir: cfg.WorkingDir,

internal/acp/agent.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,35 @@ func (a *ScriptschnellAIAgent) handleClearCommand(session *statcodeSession) stri
287287

288288
logger.Debug("handleClearCommand[%s]: clearing session", session.sessionID)
289289

290+
// Auto-save the current session before clearing if it has messages
291+
saved := false
292+
currentSession := session.orchestrator.GetSession()
293+
if currentSession != nil && len(currentSession.GetMessages()) > 0 {
294+
if storageRef, exists := session.orchestrator.GetActor("session_storage"); exists {
295+
// Generate session title (best-effort)
296+
if err := session.orchestrator.GenerateSessionTitle(context.Background()); err != nil {
297+
logger.Warn("handleClearCommand[%s]: failed to generate title: %v", session.sessionID, err)
298+
}
299+
300+
name := actor.GenerateSessionName("")
301+
if err := actor.SaveSessionViaActor(context.Background(), storageRef, currentSession, name); err != nil {
302+
logger.Warn("handleClearCommand[%s]: failed to auto-save session: %v", session.sessionID, err)
303+
} else {
304+
logger.Info("handleClearCommand[%s]: auto-saved session %s as '%s'", session.sessionID, currentSession.ID, name)
305+
saved = true
306+
}
307+
}
308+
}
309+
290310
if err := session.orchestrator.ClearSession(); err != nil {
291311
logger.Warn("handleClearCommand[%s]: failed to clear session: %v", session.sessionID, err)
292312
return fmt.Sprintf("⚠️ Failed to clear session: %v", err)
293313
}
294314

295-
response := "🧹 Conversation context and todos cleared.\n\n"
296-
response += "Ready for a fresh start! What would you like to work on?\n"
297-
298-
return response
315+
if saved {
316+
return "🧹 Session saved and cleared.\n\nReady for a fresh start! What would you like to work on?\n"
317+
}
318+
return "🧹 Conversation context and todos cleared.\n\nReady for a fresh start! What would you like to work on?\n"
299319
}
300320

301321
// handleContextCommand handles the /context command

internal/session/session.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"crypto/rand"
66
"encoding/hex"
7-
"fmt"
87
"os"
98
"path/filepath"
109
"strings"
@@ -177,14 +176,9 @@ func createShellTempDir() string {
177176
return dirPath
178177
}
179178

180-
// GenerateID creates a random session ID (base32-ish hex, 12 chars).
179+
// GenerateID creates a human-friendly session ID (e.g. "bright-silver-falcon").
181180
func GenerateID() string {
182-
var buf [6]byte
183-
if _, err := rand.Read(buf[:]); err != nil {
184-
timestamp := time.Now().UnixNano()
185-
return fmt.Sprintf("sess-%d", timestamp)
186-
}
187-
return hex.EncodeToString(buf[:])
181+
return GenerateWordID()
188182
}
189183

190184
// AddMessage adds a message to the session

internal/session/storage.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,15 @@ func (s *SessionStorage) SaveSession(session *Session, name string) error {
285285
}
286286

287287
// Write to temporary file first (atomic write)
288-
tempPath := s.getSessionPath(session.WorkingDir, storedID) + ".tmp"
289-
logger.Debug("SaveSession: creating temp file: %s", tempPath)
290-
291-
file, err := os.Create(tempPath)
288+
// Use os.CreateTemp to avoid races between concurrent saves (e.g. auto-save + manual save)
289+
finalPath := s.getSessionPath(session.WorkingDir, storedID)
290+
file, err := os.CreateTemp(workspaceDir, storedID+".gob.tmp.*")
292291
if err != nil {
293-
logger.Error("SaveSession: failed to create temp file %s: %v", tempPath, err)
292+
logger.Error("SaveSession: failed to create temp file in %s: %v", workspaceDir, err)
294293
return fmt.Errorf("failed to create temp file: %w", err)
295294
}
295+
tempPath := file.Name()
296+
logger.Debug("SaveSession: created temp file: %s", tempPath)
296297

297298
// Encode with gob
298299
logger.Debug("SaveSession: encoding session with gob")
@@ -308,7 +309,6 @@ func (s *SessionStorage) SaveSession(session *Session, name string) error {
308309
file.Close()
309310

310311
// Atomic rename
311-
finalPath := s.getSessionPath(session.WorkingDir, storedID)
312312
logger.Debug("SaveSession: renaming %s to %s", tempPath, finalPath)
313313

314314
if err := os.Rename(tempPath, finalPath); err != nil {

internal/session/wordid.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package session
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
"math/big"
7+
)
8+
9+
var adjectives = []string{
10+
"amber", "azure", "bold", "brave", "bright",
11+
"calm", "clear", "cold", "cool", "coral",
12+
"crisp", "dark", "dawn", "deep", "dry",
13+
"dusk", "early", "east", "fair", "fast",
14+
"firm", "flat", "fresh", "frost", "full",
15+
"glad", "gold", "grand", "gray", "green",
16+
"half", "high", "hot", "icy", "iron",
17+
"keen", "kind", "late", "lean", "light",
18+
"lime", "live", "long", "low", "mild",
19+
"mint", "mist", "near", "neat", "next",
20+
"north", "odd", "old", "opal", "open",
21+
"pale", "peak", "pine", "pink", "plain",
22+
"prime", "pure", "quiet", "rare", "raw",
23+
"red", "rich", "ripe", "rose", "ruby",
24+
"rust", "safe", "sage", "silk", "slim",
25+
"slow", "soft", "south", "stark", "steel",
26+
"still", "stone", "sun", "swift", "tall",
27+
"teal", "thin", "true", "warm", "west",
28+
"white", "wide", "wild", "wise", "young",
29+
}
30+
31+
var nouns = []string{
32+
"arch", "ash", "bay", "beam", "birch",
33+
"bloom", "bolt", "brook", "cape", "cave",
34+
"cedar", "cliff", "cloud", "coast", "cove",
35+
"crane", "creek", "crest", "crow", "dale",
36+
"dart", "dawn", "delta", "dove", "drift",
37+
"dune", "dust", "elm", "ember", "fern",
38+
"field", "finch", "flame", "flint", "fog",
39+
"forge", "fox", "frost", "gale", "gate",
40+
"glen", "grove", "hare", "hawk", "heath",
41+
"hill", "hive", "hollow", "ivy", "jade",
42+
"jay", "lake", "lark", "leaf", "ledge",
43+
"lily", "loft", "maple", "marsh", "mesa",
44+
"mist", "moss", "moth", "oak", "orbit",
45+
"otter", "owl", "palm", "path", "peak",
46+
"pine", "plum", "pond", "quail", "rain",
47+
"raven", "reef", "ridge", "river", "rock",
48+
"sage", "shore", "sky", "slope", "snow",
49+
"spark", "spruce", "star", "stone", "storm",
50+
"stream", "swift", "thorn", "tide", "trail",
51+
"vale", "vine", "wave", "willow", "wolf",
52+
}
53+
54+
// GenerateWordID generates a human-friendly ID in the form "adjective-adjective-noun".
55+
func GenerateWordID() string {
56+
adj1 := pickRandom(adjectives)
57+
adj2 := pickRandom(adjectives)
58+
noun := pickRandom(nouns)
59+
return fmt.Sprintf("%s-%s-%s", adj1, adj2, noun)
60+
}
61+
62+
func pickRandom(list []string) string {
63+
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(list))))
64+
if err != nil {
65+
// Fallback: just use first element (should never happen)
66+
return list[0]
67+
}
68+
return list[n.Int64()]
69+
}

0 commit comments

Comments
 (0)