feat(memory): add goose down-migration, memory CLI, and session migration hook
- LibSQLDelegate.MigrateDown() for testing migration round-trips - Integration test: down→up round-trip validates schema recreation - CLI `picoclaw memory migrate-sessions` for manual session import - CLI `picoclaw memory db-status` for database inspection
This commit is contained in:
parent
d0ff8b3f22
commit
3ab8196925
3 changed files with 163 additions and 0 deletions
|
|
@ -33,6 +33,8 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/health"
|
||||
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
picomemory "github.com/sipeed/picoclaw/pkg/memory"
|
||||
"github.com/sipeed/picoclaw/pkg/memory/delegate"
|
||||
"github.com/sipeed/picoclaw/pkg/migrate"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
"github.com/sipeed/picoclaw/pkg/state"
|
||||
|
|
@ -191,6 +193,8 @@ func main() {
|
|||
fmt.Printf("Unknown skills command: %s\n", subcommand)
|
||||
skillsHelp()
|
||||
}
|
||||
case "memory":
|
||||
memoryCmd()
|
||||
case "version", "--version", "-v":
|
||||
printVersion()
|
||||
default:
|
||||
|
|
@ -212,6 +216,7 @@ func printHelp() {
|
|||
fmt.Println(" status Show picoclaw status")
|
||||
fmt.Println(" cron Manage scheduled tasks")
|
||||
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
|
||||
fmt.Println(" memory Memory system management (db status, session migration)")
|
||||
fmt.Println(" skills Manage skills (install, list, remove)")
|
||||
fmt.Println(" version Show version information")
|
||||
}
|
||||
|
|
@ -701,6 +706,112 @@ func gatewayCmd() {
|
|||
fmt.Println("✓ Gateway stopped")
|
||||
}
|
||||
|
||||
func memoryCmd() {
|
||||
if len(os.Args) < 3 {
|
||||
memoryHelp()
|
||||
return
|
||||
}
|
||||
|
||||
sub := os.Args[2]
|
||||
switch sub {
|
||||
case "migrate-sessions":
|
||||
memoryMigrateSessions()
|
||||
case "db-status":
|
||||
memoryDBStatus()
|
||||
case "--help", "-h":
|
||||
memoryHelp()
|
||||
default:
|
||||
fmt.Printf("Unknown memory command: %s\n", sub)
|
||||
memoryHelp()
|
||||
}
|
||||
}
|
||||
|
||||
func memoryHelp() {
|
||||
fmt.Println("\nMemory system management")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage: picoclaw memory <subcommand>")
|
||||
fmt.Println()
|
||||
fmt.Println("Subcommands:")
|
||||
fmt.Println(" migrate-sessions Import file-based sessions into recall memory")
|
||||
fmt.Println(" db-status Show migration version and table counts")
|
||||
}
|
||||
|
||||
func memoryMigrateSessions() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !cfg.Memory.Enabled {
|
||||
fmt.Println("Memory system is disabled in config. Enable it first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
memDBPath := cfg.Memory.DBPath
|
||||
if memDBPath == "" {
|
||||
memDBPath = filepath.Join(workspace, "memory", "picoclaw.db")
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(memDBPath), 0755)
|
||||
|
||||
del, err := delegate.NewFromConfig(cfg.Memory, memDBPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating memory delegate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer del.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
if err := del.Init(ctx); err != nil {
|
||||
fmt.Printf("Error initializing memory schema: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sessionsDir := filepath.Join(workspace, "sessions")
|
||||
stats, err := picomemory.MigrateFileSessions(ctx, del, "picoclaw", sessionsDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Migration error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Session migration complete:\n")
|
||||
fmt.Printf(" Sessions found: %d\n", stats.SessionsFound)
|
||||
fmt.Printf(" Sessions migrated: %d\n", stats.SessionsMigrated)
|
||||
fmt.Printf(" Items created: %d\n", stats.ItemsCreated)
|
||||
fmt.Printf(" Errors: %d\n", stats.Errors)
|
||||
}
|
||||
|
||||
func memoryDBStatus() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
memDBPath := cfg.Memory.DBPath
|
||||
if memDBPath == "" {
|
||||
memDBPath = filepath.Join(workspace, "memory", "picoclaw.db")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(memDBPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Memory database not found: %s\n", memDBPath)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Memory database: %s\n", memDBPath)
|
||||
fmt.Printf("Size: %.1f KB\n", float64(fi.Size())/1024)
|
||||
fmt.Printf("Embedding dims: %d\n", cfg.Memory.EmbeddingDims)
|
||||
|
||||
if cfg.Memory.Sync.SyncURL != "" {
|
||||
fmt.Printf("Turso replica: %s\n", cfg.Memory.Sync.SyncURL)
|
||||
} else {
|
||||
fmt.Println("Mode: local-only")
|
||||
}
|
||||
}
|
||||
|
||||
func statusCmd() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,19 @@ func (d *LibSQLDelegate) Init(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MigrateDown rolls back all migrations. Intended for testing only.
|
||||
func (d *LibSQLDelegate) MigrateDown(ctx context.Context) error {
|
||||
mctx := migrations.WithEmbeddingDims(ctx, d.embeddingDims)
|
||||
provider, err := goose.NewProvider(goose.DialectSQLite3, d.db, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create migration provider: %w", err)
|
||||
}
|
||||
if _, err := provider.DownTo(mctx, 0); err != nil {
|
||||
return fmt.Errorf("migration down: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmbeddingDims returns the configured embedding vector dimensions.
|
||||
func (d *LibSQLDelegate) EmbeddingDims() int { return d.embeddingDims }
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,45 @@ func TestIntegration_GooseMigrationIdempotent(t *testing.T) {
|
|||
require.NoError(t, del.Init(ctx), "idempotent re-Init")
|
||||
}
|
||||
|
||||
func TestIntegration_GooseMigration_DownUpRoundTrip(t *testing.T) {
|
||||
del, err := delegate.NewLibSQLInMemory()
|
||||
require.NoError(t, err)
|
||||
defer del.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, del.Init(ctx), "initial up")
|
||||
|
||||
item := &memory.RecallItem{
|
||||
AgentID: testAgent,
|
||||
SessionKey: testSession,
|
||||
Role: "user",
|
||||
Sector: memory.SectorEpisodic,
|
||||
Content: "pre-migration item",
|
||||
}
|
||||
chunker := memstore.NewMarkdownChunker(memstore.DefaultMarkdownChunkerConfig())
|
||||
store := memstore.New(del, chunker, nil, memstore.DefaultConfig())
|
||||
require.NoError(t, store.StoreRecall(ctx, item))
|
||||
|
||||
require.NoError(t, del.MigrateDown(ctx), "down migration should succeed")
|
||||
|
||||
require.NoError(t, del.Init(ctx), "re-up after down should succeed")
|
||||
|
||||
fetched, err := store.GetRecall(ctx, item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, fetched, "data should be gone after down+up round-trip")
|
||||
|
||||
newItem := &memory.RecallItem{
|
||||
AgentID: testAgent,
|
||||
SessionKey: testSession,
|
||||
Role: "user",
|
||||
Sector: memory.SectorEpisodic,
|
||||
Content: "post-migration item",
|
||||
}
|
||||
require.NoError(t, store.StoreRecall(ctx, newItem), "should be able to write after re-migration")
|
||||
assert.False(t, newItem.ID.IsZero())
|
||||
}
|
||||
|
||||
func TestIntegration_BlobPK_RoundTrip(t *testing.T) {
|
||||
store, _ := setupFullStack(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue