picoclaw/pkg/session/migrate.go
dj-oyu 6062b17ba5 feat(session): implement SQLite SessionStore + LegacyAdapter (TASKS-3 Phase 0)
Replace JSON file-based session storage with SQLite backend while
maintaining full backward compatibility through LegacyAdapter.

New files:
- pkg/session/types.go: Turn, SessionInfo, CreateOpts, ListFilter types
- pkg/session/store.go: SessionStore interface (15 methods)
- pkg/session/sqlite.go: SQLite implementation (WAL mode, modernc.org/sqlite)
- pkg/session/legacy_adapter.go: wraps SessionStore with SessionManager API
- pkg/session/migrate.go: JSON → SQLite migration at startup

Changes:
- pkg/agent/instance.go: Sessions type changed to *LegacyAdapter
- loop.go / loop_test.go: zero changes (method signatures identical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:55:13 +09:00

123 lines
2.2 KiB
Go

package session
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
)
// MigrateJSONSessions reads JSON session files from jsonDir and imports them
// into the given SessionStore. Successfully migrated files are renamed to
// .json.migrated so they are skipped on subsequent runs.
//
// Individual file errors are logged and skipped (the file remains for retry).
// Returns the number of sessions migrated and the first error encountered, if any.
func MigrateJSONSessions(jsonDir string, store SessionStore) (int, error) {
entries, err := os.ReadDir(jsonDir)
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
migrated := 0
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(name, ".json") {
continue
}
path := filepath.Join(jsonDir, name)
data, err := os.ReadFile(path)
if err != nil {
log.Printf("session migrate: read %s: %v", name, err)
continue
}
var sess Session
if err := json.Unmarshal(data, &sess); err != nil {
log.Printf("session migrate: parse %s: %v", name, err)
continue
}
if sess.Key == "" {
log.Printf("session migrate: skip %s: empty key", name)
continue
}
// Create session in store (skip if already exists)
if existing, _ := store.Get(sess.Key); existing != nil {
// Already migrated (perhaps from a previous partial run)
_ = os.Rename(path, path+".migrated")
migrated++
continue
}
if err := store.Create(sess.Key, nil); err != nil {
log.Printf("session migrate: create %s: %v", sess.Key, err)
continue
}
// Import messages as a single turn
if len(sess.Messages) > 0 {
turn := &Turn{
SessionKey: sess.Key,
Kind: TurnNormal,
Messages: sess.Messages,
CreatedAt: sess.Created,
}
if err := store.Append(sess.Key, turn); err != nil {
log.Printf("session migrate: append %s: %v", sess.Key, err)
continue
}
}
// Set summary if present
if sess.Summary != "" {
_ = store.SetSummary(sess.Key, sess.Summary)
}
// Mark as migrated
if err := os.Rename(path, path+".migrated"); err != nil {
log.Printf("session migrate: rename %s: %v", name, err)
}
migrated++
}
return migrated, nil
}