feat: Gemini architecture improvements (WaitGroups, Provider Registry, Session Backups, Semantic Chunking)

This commit is contained in:
Bernardo 2026-03-10 19:18:47 +01:00
parent 992277b252
commit 81841df578
6 changed files with 424 additions and 184 deletions

View file

@ -49,6 +49,7 @@ type AgentLoop struct {
transcriber voice.Transcriber transcriber voice.Transcriber
cmdRegistry *commands.Registry cmdRegistry *commands.Registry
version string version string
wg sync.WaitGroup
} }
// processOptions configures how a message is processed // processOptions configures how a message is processed
@ -430,6 +431,10 @@ func (al *AgentLoop) Stop() {
al.running.Store(false) al.running.Store(false)
} }
func (al *AgentLoop) Wait() {
al.wg.Wait()
}
func (al *AgentLoop) RegisterTool(tool tools.Tool) { func (al *AgentLoop) RegisterTool(tool tools.Tool) {
for _, agentID := range al.registry.ListAgentIDs() { for _, agentID := range al.registry.ListAgentIDs() {
if agent, ok := al.registry.GetAgent(agentID); ok { if agent, ok := al.registry.GetAgent(agentID); ok {
@ -1122,12 +1127,16 @@ func (al *AgentLoop) runLLMIteration(
return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err) return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err)
} }
go al.handleReasoning( al.wg.Add(1)
ctx, go func() {
response.Reasoning, defer al.wg.Done()
opts.Channel, al.handleReasoning(
al.targetReasoningChannelID(opts.Channel), ctx,
) response.Reasoning,
opts.Channel,
al.targetReasoningChannelID(opts.Channel),
)
}()
logger.DebugCF("agent", "LLM response", logger.DebugCF("agent", "LLM response",
map[string]any{ map[string]any{
@ -1402,7 +1411,9 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
if len(newHistory) > agent.SummarizeMessageThreshold || tokenEstimate > threshold { if len(newHistory) > agent.SummarizeMessageThreshold || tokenEstimate > threshold {
summarizeKey := agent.ID + ":" + sessionKey summarizeKey := agent.ID + ":" + sessionKey
if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading { if _, loading := al.summarizing.LoadOrStore(summarizeKey, true); !loading {
al.wg.Add(1)
go func() { go func() {
defer al.wg.Done()
defer al.summarizing.Delete(summarizeKey) defer al.summarizing.Delete(summarizeKey)
logger.Debug("Memory threshold reached. Optimizing conversation history...") logger.Debug("Memory threshold reached. Optimizing conversation history...")
al.summarizeSession(agent, sessionKey) al.summarizeSession(agent, sessionKey)

View file

@ -42,7 +42,11 @@ func SplitMessage(content string, maxLen int) []string {
end := start + effectiveLimit end := start + effectiveLimit
// Find natural split point within the effective limit // Find natural split point within the effective limit
msgEnd := findLastNewlineInRange(runes, start, end, 200) // Try double-newline (\n\n) first for semantic paragraph splitting.
msgEnd := findLastDoubleNewlineInRange(runes, start, end, 300)
if msgEnd <= start {
msgEnd = findLastNewlineInRange(runes, start, end, 200)
}
if msgEnd <= start { if msgEnd <= start {
msgEnd = findLastSpaceInRange(runes, start, end, 100) msgEnd = findLastSpaceInRange(runes, start, end, 100)
} }
@ -206,3 +210,16 @@ func findLastSpaceInRange(runes []rune, start, end, searchWindow int) int {
} }
return start - 1 return start - 1
} }
// findLastDoubleNewlineInRange finds the last \n\n within the last searchWindow runes
// of the range runes[start:end]. Returns the absolute index of the second newline or start-1.
func findLastDoubleNewlineInRange(runes []rune, start, end, searchWindow int) int {
searchStart := max(end-searchWindow, start)
for i := end - 1; i > searchStart; i-- {
if runes[i] == '\n' && runes[i-1] == '\n' {
return i
}
}
return start - 1
}

View file

@ -0,0 +1,344 @@
package providers
import (
"strings"
"github.com/sipeed/picoclaw/pkg/config"
)
func init() {
RegisterProvider("groq", groqResolver)
RegisterProvider("openai", openaiResolver)
RegisterProvider("gpt", openaiResolver)
RegisterProvider("anthropic", anthropicResolver)
RegisterProvider("claude", anthropicResolver)
RegisterProvider("openrouter", openrouterResolver)
RegisterProvider("litellm", litellmResolver)
RegisterProvider("zhipu", zhipuResolver)
RegisterProvider("glm", zhipuResolver)
RegisterProvider("gemini", geminiResolver)
RegisterProvider("google", geminiResolver)
RegisterProvider("vllm", vllmResolver)
RegisterProvider("shengsuanyun", shengsuanyunResolver)
RegisterProvider("nvidia", nvidiaResolver)
RegisterProvider("vivgrid", vivgridResolver)
RegisterProvider("deepseek", deepseekResolver)
RegisterProvider("avian", avianResolver)
RegisterProvider("mistral", mistralResolver)
RegisterProvider("minimax", minimaxResolver)
RegisterProvider("claude-cli", claudeCLIResolver)
RegisterProvider("claude-code", claudeCLIResolver)
RegisterProvider("claudecode", claudeCLIResolver)
RegisterProvider("codex-cli", codexCLIResolver)
RegisterProvider("codex-code", codexCLIResolver)
RegisterProvider("github_copilot", copilotResolver)
RegisterProvider("copilot", copilotResolver)
}
func groqResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Groq.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Groq.APIKey,
apiBase: cfg.Providers.Groq.APIBase,
proxy: cfg.Providers.Groq.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.groq.com/openai/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func openaiResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
enableWebSearch: cfg.Providers.OpenAI.WebSearch,
}
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
sel.providerType = providerTypeCodexCLIToken
return sel, true, nil
}
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
sel.providerType = providerTypeCodexAuth
return sel, true, nil
}
sel.apiKey = cfg.Providers.OpenAI.APIKey
sel.apiBase = cfg.Providers.OpenAI.APIBase
sel.proxy = cfg.Providers.OpenAI.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.openai.com/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func anthropicResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
}
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
sel.apiBase = cfg.Providers.Anthropic.APIBase
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
sel.providerType = providerTypeClaudeAuth
return sel, true, nil
}
sel.apiKey = cfg.Providers.Anthropic.APIKey
sel.apiBase = cfg.Providers.Anthropic.APIBase
sel.proxy = cfg.Providers.Anthropic.Proxy
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func openrouterResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.OpenRouter.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.OpenRouter.APIKey,
proxy: cfg.Providers.OpenRouter.Proxy,
apiBase: cfg.Providers.OpenRouter.APIBase,
}
if sel.apiBase == "" {
sel.apiBase = "https://openrouter.ai/api/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func litellmResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.LiteLLM.APIKey != "" || cfg.Providers.LiteLLM.APIBase != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.LiteLLM.APIKey,
apiBase: cfg.Providers.LiteLLM.APIBase,
proxy: cfg.Providers.LiteLLM.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "http://localhost:4000/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func zhipuResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Zhipu.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Zhipu.APIKey,
apiBase: cfg.Providers.Zhipu.APIBase,
proxy: cfg.Providers.Zhipu.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://open.bigmodel.cn/api/paas/v4"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func geminiResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Gemini.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Gemini.APIKey,
apiBase: cfg.Providers.Gemini.APIBase,
proxy: cfg.Providers.Gemini.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://generativelanguage.googleapis.com/v1beta"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func vllmResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.VLLM.APIBase != "" {
return providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.VLLM.APIKey,
apiBase: cfg.Providers.VLLM.APIBase,
proxy: cfg.Providers.VLLM.Proxy,
}, true, nil
}
return providerSelection{}, false, nil
}
func shengsuanyunResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.ShengSuanYun.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.ShengSuanYun.APIKey,
apiBase: cfg.Providers.ShengSuanYun.APIBase,
proxy: cfg.Providers.ShengSuanYun.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://router.shengsuanyun.com/api/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func nvidiaResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Nvidia.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Nvidia.APIKey,
apiBase: cfg.Providers.Nvidia.APIBase,
proxy: cfg.Providers.Nvidia.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://integrate.api.nvidia.com/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func vivgridResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Vivgrid.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Vivgrid.APIKey,
apiBase: cfg.Providers.Vivgrid.APIBase,
proxy: cfg.Providers.Vivgrid.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.vivgrid.com/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func claudeCLIResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
return providerSelection{
providerType: providerTypeClaudeCLI,
model: model,
workspace: workspace,
}, true, nil
}
func codexCLIResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
return providerSelection{
providerType: providerTypeCodexCLI,
model: model,
workspace: workspace,
}, true, nil
}
func deepseekResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.DeepSeek.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.DeepSeek.APIKey,
apiBase: cfg.Providers.DeepSeek.APIBase,
proxy: cfg.Providers.DeepSeek.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.deepseek.com/v1"
}
if model != "deepseek-chat" && model != "deepseek-reasoner" {
sel.model = "deepseek-chat"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func avianResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Avian.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Avian.APIKey,
apiBase: cfg.Providers.Avian.APIBase,
proxy: cfg.Providers.Avian.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.avian.io/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func mistralResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Mistral.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Mistral.APIKey,
apiBase: cfg.Providers.Mistral.APIBase,
proxy: cfg.Providers.Mistral.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.mistral.ai/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func minimaxResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
if cfg.Providers.Minimax.APIKey != "" {
sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
apiKey: cfg.Providers.Minimax.APIKey,
apiBase: cfg.Providers.Minimax.APIBase,
proxy: cfg.Providers.Minimax.Proxy,
}
if sel.apiBase == "" {
sel.apiBase = "https://api.minimaxi.com/v1"
}
return sel, true, nil
}
return providerSelection{}, false, nil
}
func copilotResolver(cfg *config.Config, name, model string) (providerSelection, bool, error) {
sel := providerSelection{
providerType: providerTypeGitHubCopilot,
model: model,
connectMode: cfg.Providers.GitHubCopilot.ConnectMode,
}
if cfg.Providers.GitHubCopilot.APIBase != "" {
sel.apiBase = cfg.Providers.GitHubCopilot.APIBase
} else {
sel.apiBase = "localhost:4321"
}
return sel, true, nil
}

View file

@ -47,185 +47,14 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
// First, prefer explicit provider configuration. // First, prefer explicit provider configuration.
if providerName != "" { if providerName != "" {
switch providerName { if resolver, ok := registry[providerName]; ok {
case "groq": s, resolved, err := resolver(cfg, providerName, model)
if cfg.Providers.Groq.APIKey != "" { if err != nil {
sel.apiKey = cfg.Providers.Groq.APIKey return providerSelection{}, err
sel.apiBase = cfg.Providers.Groq.APIBase
sel.proxy = cfg.Providers.Groq.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.groq.com/openai/v1"
}
} }
case "openai", "gpt": if resolved {
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" { return s, nil
sel.enableWebSearch = cfg.Providers.OpenAI.WebSearch
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
sel.providerType = providerTypeCodexCLIToken
return sel, nil
}
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
sel.providerType = providerTypeCodexAuth
return sel, nil
}
sel.apiKey = cfg.Providers.OpenAI.APIKey
sel.apiBase = cfg.Providers.OpenAI.APIBase
sel.proxy = cfg.Providers.OpenAI.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.openai.com/v1"
}
} }
case "anthropic", "claude":
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
sel.apiBase = cfg.Providers.Anthropic.APIBase
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
sel.providerType = providerTypeClaudeAuth
return sel, nil
}
sel.apiKey = cfg.Providers.Anthropic.APIKey
sel.apiBase = cfg.Providers.Anthropic.APIBase
sel.proxy = cfg.Providers.Anthropic.Proxy
if sel.apiBase == "" {
sel.apiBase = defaultAnthropicAPIBase
}
}
case "openrouter":
if cfg.Providers.OpenRouter.APIKey != "" {
sel.apiKey = cfg.Providers.OpenRouter.APIKey
sel.proxy = cfg.Providers.OpenRouter.Proxy
if cfg.Providers.OpenRouter.APIBase != "" {
sel.apiBase = cfg.Providers.OpenRouter.APIBase
} else {
sel.apiBase = "https://openrouter.ai/api/v1"
}
}
case "litellm":
if cfg.Providers.LiteLLM.APIKey != "" || cfg.Providers.LiteLLM.APIBase != "" {
sel.apiKey = cfg.Providers.LiteLLM.APIKey
sel.apiBase = cfg.Providers.LiteLLM.APIBase
sel.proxy = cfg.Providers.LiteLLM.Proxy
if sel.apiBase == "" {
sel.apiBase = "http://localhost:4000/v1"
}
}
case "zhipu", "glm":
if cfg.Providers.Zhipu.APIKey != "" {
sel.apiKey = cfg.Providers.Zhipu.APIKey
sel.apiBase = cfg.Providers.Zhipu.APIBase
sel.proxy = cfg.Providers.Zhipu.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://open.bigmodel.cn/api/paas/v4"
}
}
case "gemini", "google":
if cfg.Providers.Gemini.APIKey != "" {
sel.apiKey = cfg.Providers.Gemini.APIKey
sel.apiBase = cfg.Providers.Gemini.APIBase
sel.proxy = cfg.Providers.Gemini.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://generativelanguage.googleapis.com/v1beta"
}
}
case "vllm":
if cfg.Providers.VLLM.APIBase != "" {
sel.apiKey = cfg.Providers.VLLM.APIKey
sel.apiBase = cfg.Providers.VLLM.APIBase
sel.proxy = cfg.Providers.VLLM.Proxy
}
case "shengsuanyun":
if cfg.Providers.ShengSuanYun.APIKey != "" {
sel.apiKey = cfg.Providers.ShengSuanYun.APIKey
sel.apiBase = cfg.Providers.ShengSuanYun.APIBase
sel.proxy = cfg.Providers.ShengSuanYun.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://router.shengsuanyun.com/api/v1"
}
}
case "nvidia":
if cfg.Providers.Nvidia.APIKey != "" {
sel.apiKey = cfg.Providers.Nvidia.APIKey
sel.apiBase = cfg.Providers.Nvidia.APIBase
sel.proxy = cfg.Providers.Nvidia.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://integrate.api.nvidia.com/v1"
}
}
case "vivgrid":
if cfg.Providers.Vivgrid.APIKey != "" {
sel.apiKey = cfg.Providers.Vivgrid.APIKey
sel.apiBase = cfg.Providers.Vivgrid.APIBase
sel.proxy = cfg.Providers.Vivgrid.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.vivgrid.com/v1"
}
}
case "claude-cli", "claude-code", "claudecode":
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
sel.providerType = providerTypeClaudeCLI
sel.workspace = workspace
return sel, nil
case "codex-cli", "codex-code":
workspace := cfg.WorkspacePath()
if workspace == "" {
workspace = "."
}
sel.providerType = providerTypeCodexCLI
sel.workspace = workspace
return sel, nil
case "deepseek":
if cfg.Providers.DeepSeek.APIKey != "" {
sel.apiKey = cfg.Providers.DeepSeek.APIKey
sel.apiBase = cfg.Providers.DeepSeek.APIBase
sel.proxy = cfg.Providers.DeepSeek.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.deepseek.com/v1"
}
if model != "deepseek-chat" && model != "deepseek-reasoner" {
sel.model = "deepseek-chat"
}
}
case "avian":
if cfg.Providers.Avian.APIKey != "" {
sel.apiKey = cfg.Providers.Avian.APIKey
sel.apiBase = cfg.Providers.Avian.APIBase
sel.proxy = cfg.Providers.Avian.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.avian.io/v1"
}
}
case "mistral":
if cfg.Providers.Mistral.APIKey != "" {
sel.apiKey = cfg.Providers.Mistral.APIKey
sel.apiBase = cfg.Providers.Mistral.APIBase
sel.proxy = cfg.Providers.Mistral.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.mistral.ai/v1"
}
}
case "minimax":
if cfg.Providers.Minimax.APIKey != "" {
sel.apiKey = cfg.Providers.Minimax.APIKey
sel.apiBase = cfg.Providers.Minimax.APIBase
sel.proxy = cfg.Providers.Minimax.Proxy
if sel.apiBase == "" {
sel.apiBase = "https://api.minimaxi.com/v1"
}
}
case "github_copilot", "copilot":
sel.providerType = providerTypeGitHubCopilot
if cfg.Providers.GitHubCopilot.APIBase != "" {
sel.apiBase = cfg.Providers.GitHubCopilot.APIBase
} else {
sel.apiBase = "localhost:4321"
}
sel.connectMode = cfg.Providers.GitHubCopilot.ConnectMode
return sel, nil
} }
} }

22
pkg/providers/registry.go Normal file
View file

@ -0,0 +1,22 @@
package providers
import (
"github.com/sipeed/picoclaw/pkg/config"
)
// ProviderResolver is a function that attempts to resolve a provider and its configuration.
// It returns the selection, a boolean indicating if it was successfully resolved, and any error.
type ProviderResolver func(cfg *config.Config, providerName string, model string) (providerSelection, bool, error)
var (
registry = make(map[string]ProviderResolver)
)
// RegisterProvider registers a new provider resolver.
func RegisterProvider(name string, resolver ProviderResolver) {
registry[name] = resolver
}
func init() {
// Built-in providers will register themselves here or via init() in their respective files.
}

View file

@ -233,6 +233,8 @@ func (sm *SessionManager) Save(key string) error {
} }
sessionPath := filepath.Join(sm.storage, filename+".json") sessionPath := filepath.Join(sm.storage, filename+".json")
bakPath := sessionPath + ".bak"
tmpFile, err := os.CreateTemp(sm.storage, "session-*.tmp") tmpFile, err := os.CreateTemp(sm.storage, "session-*.tmp")
if err != nil { if err != nil {
return err return err
@ -262,7 +264,13 @@ func (sm *SessionManager) Save(key string) error {
return err return err
} }
// If primary exists, rename it to .bak before applying the new version.
if _, err := os.Stat(sessionPath); err == nil {
_ = os.Rename(sessionPath, bakPath)
}
if err := os.Rename(tmpPath, sessionPath); err != nil { if err := os.Rename(tmpPath, sessionPath); err != nil {
_ = os.Rename(bakPath, sessionPath)
return err return err
} }
cleanup = false cleanup = false
@ -285,6 +293,8 @@ func (sm *SessionManager) loadSessions() error {
} }
sessionPath := filepath.Join(sm.storage, file.Name()) sessionPath := filepath.Join(sm.storage, file.Name())
bakPath := sessionPath + ".bak"
data, err := os.ReadFile(sessionPath) data, err := os.ReadFile(sessionPath)
if err != nil { if err != nil {
continue continue
@ -292,6 +302,13 @@ func (sm *SessionManager) loadSessions() error {
var session Session var session Session
if err := json.Unmarshal(data, &session); err != nil { if err := json.Unmarshal(data, &session); err != nil {
// Try to recover from backup
if bakData, bakErr := os.ReadFile(bakPath); bakErr == nil {
if err := json.Unmarshal(bakData, &session); err == nil {
sm.sessions[session.Key] = &session
continue
}
}
continue continue
} }