MemoDocumentation
EN

Backend Architecture

The Memo backend is a monolithic Go application structured into 29 packages around a central App orchestrator. It serves a plain REST + SSE API over localhost:8090 using only the standard library's http.ServeMux.

Entry Point

// main.go
func main() {
    port := flag.Int("port", 8090, "HTTP server port")
    flag.Parse()
    app, _ := app.New()
    app.Start()
    webserver.ListenAndServe(app, *port)
}

app.New() initializes all subsystems. webserver.ListenAndServe registers routes and starts the HTTP server.

Central Orchestrator: internal/app/

The App struct is the brains of the backend:

type App struct {
    cfg        *config.Config
    store      *memory.Store
    provider   *provider.Router
    conductor  *orchestra.Conductor
    executor   *agent.Executor
    syncer     *cloudsync.Drive
    waclient   *whatsapp.Client
    calendar   *calendar.Store
    sessions   *sessions.Manager
    identity   *identity.Manager
    skills     *skill.Manager
    mood       *mood.Engine
    db         *database.DB
    // ...
}

Key methods on App:

Method File Purpose
Chat() internal/app/chat.go Full chat pipeline — prompt construction, memory retrieval, provider routing, streaming
ChatStream() internal/app/llm.go Token-level SSE streaming wrapper
RememberExchange() internal/app/memory.go Store conversation turn in RAG
SearchMemory() internal/app/memory.go Hybrid search with RRF
ExecuteAgent() internal/app/agent.go Tool-calling pipeline
RunOrchestra() internal/app/orchestra.go Multi-model parallel execution

Bridge Pattern

The internal/webserver/ package uses a bridge pattern to decouple HTTP handlers from the App:

// internal/webserver/bridge.go

type AppBridge interface {
    Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
    ChatStream(ctx context.Context, req ChatRequest, ch chan<- SSEEvent) error
    // ... ~90 method signatures
}

type FullBridge struct {
    app *app.App
}

func (b *FullBridge) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
    return b.app.Chat(ctx, req)
}

Handlers only know about the bridge interface — never the concrete App. This enables testing handlers with mock bridges and future architectural changes without touching HTTP code.

API Layer: internal/webserver/

Route Registration

// internal/webserver/server.go
func ListenAndServe(app *app.App, port int) error {
    mux := http.NewServeMux()
    bridge := &FullBridge{app: app}
    registerFlutterRoutes(mux, bridge)
    return http.ListenAndServe(fmt.Sprintf(":%d", port), mux)
}

Route Categories

Prefix Count Purpose
/api/chat 3 Send message, stream response, stop
/api/memory/ 8 Search, remember, forget, export, import, analytics, debug, settings
/api/providers/ 10 CRUD + test for each provider type
/api/models/ 6 Discover, download, list, delete, HuggingFace catalog
/api/agent/ 4 Execute, permissions, pipeline status
/api/orchestra/ 5 Run, config, roles, status
/api/calendar/ 5 CRUD events, settings, reminders
/api/sync/ 6 Push, pull, status, OAuth, settings
/api/whatsapp/ 8 Pair, status, messages, send, contacts, search
/api/skills/ 6 List, get, install, remove, activate
/api/config/ 4 Get, set, export, import
/api/mood/ 3 Score, settings, toggle
/api/proactive/ 4 Settings, suggestions, history, trigger
/api/sessions/ 4 List, get, delete, rename
/api/llama/ 5 Start, stop, status, GPU info, models
/api/system/ 3 Version, health, shutdown

SSE Streaming

Chat and agent responses use Server-Sent Events:

// internal/webserver/handlers_flutter.go
func handleChatStream(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    flusher, _ := w.(http.Flusher)
    eventCh := make(chan SSEEvent, 100)

    go bridge.ChatStream(r.Context(), req, eventCh)

    for event := range eventCh {
        fmt.Fprintf(w, "data: %s\n\n", event.JSON())
        flusher.Flush()
    }
}

Events include: token, thinking, tool_executing, tool_result, memory_retrieved, error, done.

Provider System: internal/provider/

The provider router manages multiple LLM backends:

// internal/provider/router.go
type Router struct {
    providers map[string]Provider  // keyed by provider name
    order     []string             // priority order (fallback chain)
    mu        sync.RWMutex
}

type Provider interface {
    Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
    ChatStream(ctx context.Context, req ChatRequest, ch chan<- TokenEvent) error
    Health(ctx context.Context) bool
}

Supported Providers

Provider File API Type
llama.cpp llama.go Local, OpenAI-compatible
openai openai.go Cloud, OpenAI API
gemini gemini.go Cloud, Gemini API
claude claude.go Cloud, Anthropic API
grok grok.go Cloud, xAI API
groq groq.go Cloud, Groq API
openrouter openrouter.go Cloud, OpenRouter API
ollama ollama.go Local, Ollama API

Fallback Chain

func (r *Router) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
    for _, name := range r.order {
        provider := r.providers[name]
        if !provider.Enabled() {
            continue
        }
        resp, err := provider.Chat(ctx, req)
        if err == nil {
            return resp, nil
        }
        provider.RecordFailure()
        // Continue to next provider
    }
    return nil, ErrAllProvidersFailed
}

After 3 consecutive failures, a provider is auto-disabled. A background health-check goroutine polls disabled providers and re-enables them when they recover.

Memory Store: internal/memory/

// internal/memory/store.go
type Store struct {
    db         *sql.DB
    embedModel string
    mu         sync.RWMutex
}

func (s *Store) Search(ctx context.Context, query string, topK int) ([]Memory, error)
func (s *Store) Store(ctx context.Context, exchange Exchange) error
func (s *Store) Forget(ctx context.Context, pattern string) (int, error)
func (s *Store) Export(ctx context.Context) ([]byte, error)
func (s *Store) Import(ctx context.Context, data []byte) error

storeMu (sync.RWMutex) protects against concurrent memory re-initialization. Never read or write the store without holding the appropriate lock.

Agent Executor: internal/agent/

// internal/agent/executor.go
type Executor struct {
    tools       map[string]Tool
    permissions *PermissionPolicy
    sandbox     *Sandbox
    pipeline    *Pipeline
}

func (e *Executor) Execute(ctx context.Context, task string, events chan<- AgentEvent) (string, error)

Sandbox Rules

  • Symlinks resolved with filepath.EvalSymlinks before path boundary checks
  • Write operations checked against allowlist directories
  • run_command runs via bash -c with 60s timeout
  • File reads limited to project directory
  • WriteFile content cap: 10 MB

Orchestra Conductor: internal/orchestra/

// internal/orchestra/conductor.go
type Conductor struct {
    roles      map[string]Role  // 8 roles
    chiefModel string
}

type Role struct {
    Name         string   // planner, frontend, backend, ...
    Provider     string
    Model        string
    SystemPrompt string
    Enabled      bool
}

The conductor:

  1. Sends the user task to the chief model for decomposition
  2. Dispatches sub-tasks to enabled roles in parallel (goroutine per role)
  3. Collects results, sends to chief for synthesis
  4. Streams progress events (orchestra:role_started, orchestra:role_completed)

Database Layer: internal/database/

// internal/database/db.go
type DB struct {
    Write chan func(*sql.DB)  // Serialized write queue
    Read  *sql.DB             // Concurrent reads
}

All SQLite writes go through the Write channel to prevent database is locked errors. Reads can happen concurrently on the Read handle. This is particularly important for the memory store, where embedding writes and FTS index updates must be atomic.

Lifecycle

Startup:
  config.Load() → db.Open() → provider.Init() → memory.Init()
  → agent.Init() → cloudsync.Init() → whatsapp.Init()
  → calendar.Init() → mood.Init() → webserver.ListenAndServe()

Shutdown:
  webserver.Shutdown() [stop accepting new requests]
  → whatsapp.Stop() → cloudsync.Stop() → calendar.Stop()
  → mood.Stop() → memory.Close() → db.Close()


Webserver must stop first on shutdown. Stopping subsystems before the webserver causes in-flight HTTP requests to access nil/invalid resources and crash.

Key Design Rules

  1. No external HTTP routerhttp.ServeMux only
  2. Bridge pattern — handlers never reference App directly
  3. context.Context as first parameter — never stored in struct fields (except lifecycle goroutines)
  4. sync.RWMutex for shared state — document why if you deviate
  5. SQLite writes serialized — use database.DB.Write channel
  6. CGO requiredCGO_ENABLED=1 for all Go commands
  7. Atomic writes — temp file + rename for all config/data persistence