Files
Plexum-gemini-provider/internal/runner/session_mutate.go
hzhang dbe68a889c feat(gemini): SessionMutator stub + session-file resolver
Wires the host's mirror hook end-to-end: FindGeminiSessionFile resolves
~/.gemini/tmp/<workspace>/chats/session-<ISO>-<sid_prefix>.jsonl from
the session id captured into .plexum-gemini-session, picking the
most-recent match by mtime. Logs the no-op for telemetry.

Real rewrite is deferred to v2: gemini-cli's tool-call id namespace
doesn't match Plexum's canonical block format (and its on-disk shape
is `$set`-style mutation log rather than flat append-only). When
tool-call round-trip through gemini's native surface lands, the
rewriter plugs into this file's path resolver.
2026-06-01 14:01:33 +01:00

92 lines
2.5 KiB
Go

// 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/<workspace-basename>/chats/session-<ISO>-<sid>.jsonl
//
// where <sid> 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
}