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.
This commit is contained in:
@@ -125,6 +125,35 @@ func (p *geminiPlugin) StreamWithAgent(ctx context.Context, modelID string, agen
|
|||||||
return runner.Run(ctx, p.host, agent, cliModel, p.cfg.Binary, p.cfg.ExtraArgs, req)
|
return runner.Run(ctx, p.host, agent, cliModel, p.cfg.Binary, p.cfg.ExtraArgs, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MutateSession is the session-mirror hook (decision #29x). gemini-cli
|
||||||
|
// stores transcripts under ~/.gemini/tmp/<workspace-name>/chats/
|
||||||
|
// session-<ISO>-<session_id_prefix>.jsonl with a `$set`-style mutation
|
||||||
|
// log (each line either a session-meta header or `{"$set":{...}}`
|
||||||
|
// applied in order). Tool-call records use gemini-cli's own id
|
||||||
|
// namespace (no shape doc as of this writing) which doesn't match
|
||||||
|
// Plexum's toolu_*-style block ids.
|
||||||
|
//
|
||||||
|
// v1 ships as a logged no-op: we resolve the file path so operator
|
||||||
|
// telemetry confirms wiring, but we don't attempt to rewrite. When
|
||||||
|
// Plexum starts routing tool-call ids through gemini's native surface
|
||||||
|
// (preserving the round-trip), the rewriter plugs in here.
|
||||||
|
func (p *geminiPlugin) MutateSession(ctx context.Context, req plugin.SessionMutateRequest) error {
|
||||||
|
path, err := runner.FindGeminiSessionFile(req.Workspace)
|
||||||
|
if err != nil {
|
||||||
|
p.host.Log("debug", "gemini session mutate: find failed", map[string]any{
|
||||||
|
"agent": req.AgentID, "err": err.Error(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.host.Log("info", "gemini session mutate (no-op v1)", map[string]any{
|
||||||
|
"agent": req.AgentID,
|
||||||
|
"session_path": path,
|
||||||
|
"requested": len(req.Mutations),
|
||||||
|
"reason": "gemini tool-call id round-trip not yet wired",
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// mapModel converts a Plexum-advertised model id into the CLI's
|
// mapModel converts a Plexum-advertised model id into the CLI's
|
||||||
// --model value. Returns "" for "gemini" (use CLI default) and
|
// --model value. Returns "" for "gemini" (use CLI default) and
|
||||||
// passes any other unrecognized id through verbatim (operator may
|
// passes any other unrecognized id through verbatim (operator may
|
||||||
|
|||||||
91
internal/runner/session_mutate.go
Normal file
91
internal/runner/session_mutate.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user