mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
225 lines
6.8 KiB
Go
225 lines
6.8 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/memory"
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
)
|
|
|
|
// JSONLBackend adapts a memory.Store into the SessionStore interface.
|
|
// Write errors are logged rather than returned, matching the fire-and-forget
|
|
// contract of SessionManager that the agent loop relies on.
|
|
type JSONLBackend struct {
|
|
store memory.Store
|
|
}
|
|
|
|
type metaAwareStore interface {
|
|
GetSessionMeta(ctx context.Context, sessionKey string) (memory.SessionMeta, error)
|
|
UpsertSessionMeta(ctx context.Context, sessionKey string, scope json.RawMessage, aliases []string) error
|
|
ResolveSessionKey(ctx context.Context, sessionKey string) (string, bool, error)
|
|
}
|
|
|
|
// MetadataAwareSessionStore exposes structured session metadata operations.
|
|
type MetadataAwareSessionStore interface {
|
|
EnsureSessionMetadata(sessionKey string, scope *SessionScope, aliases []string)
|
|
ResolveSessionKey(sessionKey string) string
|
|
GetSessionScope(sessionKey string) *SessionScope
|
|
}
|
|
|
|
// NewJSONLBackend wraps a memory.Store for use as a SessionStore.
|
|
func NewJSONLBackend(store memory.Store) *JSONLBackend {
|
|
return &JSONLBackend{store: store}
|
|
}
|
|
|
|
func (b *JSONLBackend) resolveSessionKey(sessionKey string) string {
|
|
metaStore, ok := b.store.(metaAwareStore)
|
|
if !ok {
|
|
return sessionKey
|
|
}
|
|
resolved, found, err := metaStore.ResolveSessionKey(context.Background(), sessionKey)
|
|
if err != nil {
|
|
log.Printf("session: resolve session key: %v", err)
|
|
return sessionKey
|
|
}
|
|
if found && resolved != "" {
|
|
return resolved
|
|
}
|
|
return sessionKey
|
|
}
|
|
|
|
// ResolveSessionKey maps aliases onto their canonical session key when the
|
|
// underlying store supports structured metadata. Unknown aliases fall back to
|
|
// the original input so existing callers remain compatible.
|
|
func (b *JSONLBackend) ResolveSessionKey(sessionKey string) string {
|
|
return b.resolveSessionKey(sessionKey)
|
|
}
|
|
|
|
// EnsureSessionMetadata persists scope and alias metadata for a session.
|
|
func (b *JSONLBackend) EnsureSessionMetadata(sessionKey string, scope *SessionScope, aliases []string) {
|
|
metaStore, ok := b.store.(metaAwareStore)
|
|
if !ok {
|
|
return
|
|
}
|
|
sessionKey = strings.TrimSpace(sessionKey)
|
|
if sessionKey == "" {
|
|
return
|
|
}
|
|
|
|
var rawScope json.RawMessage
|
|
if scope != nil {
|
|
data, err := json.Marshal(scope)
|
|
if err != nil {
|
|
log.Printf("session: encode session scope: %v", err)
|
|
return
|
|
}
|
|
rawScope = data
|
|
}
|
|
ctx := context.Background()
|
|
if err := metaStore.UpsertSessionMeta(ctx, sessionKey, rawScope, aliases); err != nil {
|
|
log.Printf("session: upsert session metadata: %v", err)
|
|
return
|
|
}
|
|
|
|
canonicalHistory, historyErr := b.store.GetHistory(ctx, sessionKey)
|
|
if historyErr != nil {
|
|
log.Printf("session: get canonical history: %v", historyErr)
|
|
return
|
|
}
|
|
canonicalSummary, summaryErr := b.store.GetSummary(ctx, sessionKey)
|
|
if summaryErr != nil {
|
|
log.Printf("session: get canonical summary: %v", summaryErr)
|
|
return
|
|
}
|
|
if len(canonicalHistory) > 0 || strings.TrimSpace(canonicalSummary) != "" {
|
|
return
|
|
}
|
|
|
|
for _, alias := range aliases {
|
|
alias = strings.TrimSpace(alias)
|
|
if alias == "" || alias == sessionKey {
|
|
continue
|
|
}
|
|
aliasHistory, err := b.store.GetHistory(ctx, alias)
|
|
if err != nil {
|
|
log.Printf("session: get alias history: %v", err)
|
|
continue
|
|
}
|
|
aliasSummary, err := b.store.GetSummary(ctx, alias)
|
|
if err != nil {
|
|
log.Printf("session: get alias summary: %v", err)
|
|
continue
|
|
}
|
|
if len(aliasHistory) == 0 && strings.TrimSpace(aliasSummary) == "" {
|
|
continue
|
|
}
|
|
if err := b.store.SetHistory(ctx, sessionKey, aliasHistory); err != nil {
|
|
log.Printf("session: promote alias history: %v", err)
|
|
return
|
|
}
|
|
if strings.TrimSpace(aliasSummary) != "" {
|
|
if err := b.store.SetSummary(ctx, sessionKey, aliasSummary); err != nil {
|
|
log.Printf("session: promote alias summary: %v", err)
|
|
}
|
|
}
|
|
if err := metaStore.UpsertSessionMeta(ctx, sessionKey, rawScope, aliases); err != nil {
|
|
log.Printf("session: refresh session metadata after promotion: %v", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// GetSessionScope reads structured scope metadata for a session key or alias.
|
|
func (b *JSONLBackend) GetSessionScope(sessionKey string) *SessionScope {
|
|
metaStore, ok := b.store.(metaAwareStore)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
sessionKey = b.resolveSessionKey(sessionKey)
|
|
meta, err := metaStore.GetSessionMeta(context.Background(), sessionKey)
|
|
if err != nil {
|
|
log.Printf("session: get session metadata: %v", err)
|
|
return nil
|
|
}
|
|
if len(meta.Scope) == 0 {
|
|
return nil
|
|
}
|
|
var scope SessionScope
|
|
if err := json.Unmarshal(meta.Scope, &scope); err != nil {
|
|
log.Printf("session: decode session scope: %v", err)
|
|
return nil
|
|
}
|
|
return CloneScope(&scope)
|
|
}
|
|
|
|
func (b *JSONLBackend) AddMessage(sessionKey, role, content string) {
|
|
sessionKey = b.resolveSessionKey(sessionKey)
|
|
if err := b.store.AddMessage(context.Background(), sessionKey, role, content); err != nil {
|
|
log.Printf("session: add message: %v", err)
|
|
}
|
|
}
|
|
|
|
func (b *JSONLBackend) AddFullMessage(sessionKey string, msg providers.Message) {
|
|
sessionKey = b.resolveSessionKey(sessionKey)
|
|
if err := b.store.AddFullMessage(context.Background(), sessionKey, msg); err != nil {
|
|
log.Printf("session: add full message: %v", err)
|
|
}
|
|
}
|
|
|
|
func (b *JSONLBackend) GetHistory(key string) []providers.Message {
|
|
key = b.resolveSessionKey(key)
|
|
msgs, err := b.store.GetHistory(context.Background(), key)
|
|
if err != nil {
|
|
log.Printf("session: get history: %v", err)
|
|
return []providers.Message{}
|
|
}
|
|
return msgs
|
|
}
|
|
|
|
func (b *JSONLBackend) GetSummary(key string) string {
|
|
key = b.resolveSessionKey(key)
|
|
summary, err := b.store.GetSummary(context.Background(), key)
|
|
if err != nil {
|
|
log.Printf("session: get summary: %v", err)
|
|
return ""
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func (b *JSONLBackend) SetSummary(key, summary string) {
|
|
key = b.resolveSessionKey(key)
|
|
if err := b.store.SetSummary(context.Background(), key, summary); err != nil {
|
|
log.Printf("session: set summary: %v", err)
|
|
}
|
|
}
|
|
|
|
func (b *JSONLBackend) SetHistory(key string, history []providers.Message) {
|
|
key = b.resolveSessionKey(key)
|
|
if err := b.store.SetHistory(context.Background(), key, history); err != nil {
|
|
log.Printf("session: set history: %v", err)
|
|
}
|
|
}
|
|
|
|
func (b *JSONLBackend) TruncateHistory(key string, keepLast int) {
|
|
key = b.resolveSessionKey(key)
|
|
if err := b.store.TruncateHistory(context.Background(), key, keepLast); err != nil {
|
|
log.Printf("session: truncate history: %v", err)
|
|
}
|
|
}
|
|
|
|
// Save persists session state. Since the JSONL store fsyncs every write
|
|
// immediately, the data is already durable. Save runs compaction to reclaim
|
|
// space from logically truncated messages (no-op when there are none).
|
|
func (b *JSONLBackend) Save(key string) error {
|
|
key = b.resolveSessionKey(key)
|
|
return b.store.Compact(context.Background(), key)
|
|
}
|
|
|
|
// Close releases resources held by the underlying store.
|
|
func (b *JSONLBackend) Close() error {
|
|
return b.store.Close()
|
|
}
|