// session_mutate.go — gemini session file path resolution. The actual // JSONL rewrite is deferred (see plugin main.go MutateSession comment): // gemini-cli uses its own tool-call id namespace that doesn't match // Plexum's, so v1 ships as a logged no-op. This file owns the path // resolver so the v2 rewriter has its entry point. // // Layout (observed): // // ~/.gemini/tmp//chats/session--.jsonl // // where is the first 8 chars of the session_id captured into // workspace/.plexum-gemini-session. We pick the file whose name // contains that prefix; multiple matches → the most-recent by mtime. package runner import ( "errors" "fmt" "os" "path/filepath" "sort" "strings" ) // FindGeminiSessionFile resolves the chat JSONL gemini-cli writes for // the session id captured in workspace/.plexum-gemini-session. // Returns ("", error) if no session id is recorded yet OR no chat // file is on disk. func FindGeminiSessionFile(workspace string) (string, error) { sid := loadSessionID(workspace) if sid == "" { return "", errors.New("no gemini session id captured yet") } prefix := sid if len(prefix) > 8 { prefix = prefix[:8] } home, err := os.UserHomeDir() if err != nil { return "", err } root := filepath.Join(home, ".gemini", "tmp") wsName := filepath.Base(workspace) candidates := []string{ filepath.Join(root, wsName, "chats"), // Fallback dirs (older / alternate gemini-cli versions). We // glob defensively so an operator's custom layout still // surfaces something for telemetry. } entries := []os.DirEntry{} chosenDir := "" for _, dir := range candidates { es, err := os.ReadDir(dir) if err == nil { entries = es chosenDir = dir break } } if chosenDir == "" { return "", fmt.Errorf("no gemini chats dir under %s", root) } // Pick the newest session-*.jsonl whose name contains the sid prefix. type cand struct { path string mtime int64 } var matches []cand for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { continue } if !strings.Contains(e.Name(), prefix) { continue } info, err := e.Info() if err != nil { continue } matches = append(matches, cand{ path: filepath.Join(chosenDir, e.Name()), mtime: info.ModTime().UnixNano(), }) } if len(matches) == 0 { return "", fmt.Errorf("no chat file matching session_id prefix %q in %s", prefix, chosenDir) } sort.Slice(matches, func(i, j int) bool { return matches[i].mtime > matches[j].mtime }) return matches[0].path, nil }