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/health"
|
||||||
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
"github.com/sipeed/picoclaw/pkg/heartbeat"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"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/migrate"
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
"github.com/sipeed/picoclaw/pkg/state"
|
"github.com/sipeed/picoclaw/pkg/state"
|
||||||
|
|
@ -191,6 +193,8 @@ func main() {
|
||||||
fmt.Printf("Unknown skills command: %s\n", subcommand)
|
fmt.Printf("Unknown skills command: %s\n", subcommand)
|
||||||
skillsHelp()
|
skillsHelp()
|
||||||
}
|
}
|
||||||
|
case "memory":
|
||||||
|
memoryCmd()
|
||||||
case "version", "--version", "-v":
|
case "version", "--version", "-v":
|
||||||
printVersion()
|
printVersion()
|
||||||
default:
|
default:
|
||||||
|
|
@ -212,6 +216,7 @@ func printHelp() {
|
||||||
fmt.Println(" status Show picoclaw status")
|
fmt.Println(" status Show picoclaw status")
|
||||||
fmt.Println(" cron Manage scheduled tasks")
|
fmt.Println(" cron Manage scheduled tasks")
|
||||||
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
|
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(" skills Manage skills (install, list, remove)")
|
||||||
fmt.Println(" version Show version information")
|
fmt.Println(" version Show version information")
|
||||||
}
|
}
|
||||||
|
|
@ -701,6 +706,112 @@ func gatewayCmd() {
|
||||||
fmt.Println("✓ Gateway stopped")
|
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() {
|
func statusCmd() {
|
||||||
cfg, err := loadConfig()
|
cfg, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,19 @@ func (d *LibSQLDelegate) Init(ctx context.Context) error {
|
||||||
return nil
|
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.
|
// EmbeddingDims returns the configured embedding vector dimensions.
|
||||||
func (d *LibSQLDelegate) EmbeddingDims() int { return d.embeddingDims }
|
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")
|
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) {
|
func TestIntegration_BlobPK_RoundTrip(t *testing.T) {
|
||||||
store, _ := setupFullStack(t)
|
store, _ := setupFullStack(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue