Skip to content

Add persistent store implementation (replace in-memory store) #7

@JAORMX

Description

@JAORMX

Summary

Waggle currently uses an in-memory MemoryStore (pkg/infra/store/memory.go) that implements the environment.Repository interface. All environment state is lost on restart. We need a persistent store so environments can survive server restarts and support operational use cases like auditing.

Current Architecture (DDD)

The codebase follows Domain-Driven Design with clean separation:

  • Domain interface: pkg/domain/environment/repository.go defines Repository with 5 methods: Save, FindByID, FindAll, Delete, Count
  • Current adapter: pkg/infra/store/memory.go implements Repository with a map[string]*Environment + sync.RWMutex
  • Service layer: pkg/service/environment.go depends on the Repository interface (not the concrete store)
  • Wiring: cmd/waggle/main.go:59 instantiates store.NewMemoryStore() and injects it

The service layer calls Save() after every state transition (Creating, Running, Error, Destroying) and Delete() on destroy. FindByID() and FindAll() are read-only. All methods accept context.Context.

Critical contract: copy semantics

The MemoryStore returns copies on both Save() and FindByID() to prevent aliasing bugs. The persistent store must maintain equivalent isolation — a mutated *Environment returned from FindByID() must not affect the stored state until Save() is called again.

Approach

New adapter in pkg/infra/store/

Add a new file (e.g., bolt.go or sqlite.go) implementing environment.Repository. The implementation choice depends on requirements:

Option Pros Cons
bbolt (embedded KV) Zero config, single file, no external deps No SQL, limited query flexibility
SQLite (embedded SQL) SQL queries, mature, well-understood CGO dependency (unless modernc)

For a single-binary MCP server, bbolt or modernc SQLite (pure Go) are good fits — no external database to manage.

Serialization

The Environment struct needs to be serialized/deserialized. Options:

  • JSON (simple, human-readable, already used in MCP responses)
  • Protobuf (compact, schema-versioned — overkill for now)

Wiring change

Only cmd/waggle/main.go needs to change:

// Before
repo := store.NewMemoryStore()

// After (example with bbolt)
repo, err := store.NewBoltStore(cfg.DataDir + "/waggle.db")
if err != nil {
    return fmt.Errorf("open store: %w", err)
}
defer repo.Close()

Configuration

Add a WAGGLE_STORE_PATH env var (or reuse DataDir) for the database file path. The MemoryStore should remain available for testing and as a fallback.

Acceptance Criteria

  • New Repository implementation in pkg/infra/store/ with persistent storage
  • Implements all 5 methods: Save, FindByID, FindAll, Delete, Count
  • Returns environment.ErrNotFound for missing entities (matching MemoryStore contract)
  • Copy semantics maintained (returned *Environment is independent of stored state)
  • Context respected for cancellation/timeout on all operations
  • MemoryStore kept for tests (not deleted)
  • Wiring in main.go updated to use persistent store by default
  • Configuration via environment variable for store path
  • Table-driven parallel unit tests matching memory_test.go coverage
  • doc.go with SPDX headers if new package created

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions