This document details the technical architecture, data flow, backend and frontend components, API design, and storage layers of Memo (Local LLM Memory Shell).
graph TB
subgraph Frontend["Flutter Desktop Client (localhost)"]
UI["AppShell / Screens"] -->|Riverpod| States["State Providers"]
States -->|Dio HTTP| API["MemoApiClient (508 lines)"]
end
subgraph Backend["Headless Go Server (:8090)"]
WS["Web Server<br/>(server.go + handlers_flutter.go)"]
WS -->|AppBridge / FullBridge| APP["app.go (2409 lines)"]
APP -->|SQLite + sqlite-vec| MEM["Memory Store<br/>(vec0 ANN index)"]
APP -->|sync.RWMutex| SES["Session Manager<br/>(JSON files)"]
APP -->|subprocess| LLAMA["Llama Server<br/>Manager"]
APP -->|OAuth2 + AES-256| SYNC["Cloud Sync<br/>(Google Drive)"]
APP -->|HF API| MODEL["Model Store<br/>(HF download/search)"]
APP -->|Router + Fallback| PROV["Provider System<br/>(OpenAI, Gemini, Claude...)"]
APP -->|Tool Registry + Pipeline| AGENT["Agent Engine<br/>(Tool Calling, Permissions)"]
APP -->|Chief + Roles| ORCH["Orchestra Mode<br/>(Multi-Model)"]
end
API <-->|"REST / JSON / SSE"| WS
LLAMA <-->|"stdin/stdout"| CP["llama-server Binary"]
PROV -->|"HTTP"| EXT["External LLM APIs"]
8090 (configurable via --port)application/json), Multipart for file uploadsinternal/memory/)vec0 virtual table (vector ANN) and an fts5 virtual table (keyword search) — both require -tags "sqlite_fts5" at build time or they silently disable, see CGO_FLAGS.mdsplitCompoundQuery) so each topic gets its own vector search instead of one diluted embeddingsource='explicit'/importance=5 rows (GetPinnedFacts) are injected into every prompt unconditionally, bypassing the hybrid search above entirely — populated via /remember or automatic background fact-detection (extractAndPinFacts)source='explicit' rows were excluded from general consolidation with nothing else deduping them (fixed, see BUG_REPORT.md/handoff.md TD-2)internal/sessions/)data/sessions/config/config.yaml)0644 permissions (world-readable)sequenceDiagram
participant User
participant Frontend
participant Backend
participant Embedder
participant Memory
participant LLM
User->>Frontend: Send message
Frontend->>Backend: POST /api/send/stream
Backend->>Embedder: Embed query text
Embedder-->>Backend: Query vector
Backend->>Memory: Hybrid search (vector + FTS5, RRF-merged) + pinned facts
Memory-->>Backend: Top-K relevant memories + all pinned facts
Backend->>LLM: System prompt + memories + history + query
LLM-->>Backend: Streaming tokens (SSE)
Backend-->>Frontend: SSE stream chunks
Frontend-->>User: Render tokens
Backend->>Memory: Save interaction (async)
Backend->>Memory: Background fact-extraction, pin if durable (async)
POST /api/send/streamsplitCompoundQuery), each embedded separatelyGetPinnedFacts unconditionally adds every source='explicit' memory, independent of the retrieval aboveactiveProvider set) → provider.Router with fallback chainapi.Client pointed at local llama-serverextractAndPinFacts) checks whether the turn contains a durable personal fact and pins it if soAgent mode overrides normal flow when enabled + active provider set:
SendMessageStreamroutes tocallAgentStreamwhich runs the LLM tool-calling pipeline.
This section documents the foundational modules in depth (line counts and known-issue notes are from earlier in the project's life and not re-verified per release). For the full current package list including everything added since — task loop, Live Mode v2, Telegram, the OpenAI-compatible gateway, gemini-sub, the desktop mascot, Code Mode sub-modes — see
docs/DOCS.md§4 and the file-by-filedocs/PROJECT_MAP.md.
app.go (2409 lines)Central orchestrator. Manages:
a.client, a.embeddingClient)Known issues:
a.syncManager assigned without lock → data racea.store assigned without storeMu at startupinternal/webserver/server.go (583 lines)StartHTTP (localhost) and Start (remote/TLS)Shutdown(context.Background()) can block indefinitelyinternal/webserver/handlers_flutter.go (960 lines)All Flutter-facing REST handlers:
Known issues: SSE handler doesn't monitor request.Context().Done() → orphaned streams (mitigated via context propagation)
internal/llama/llama.go (462 lines)llama-server subprocess lifecycle (start, stop, monitor, wait-ready)killByPort)monitor() goroutine accesses s.cmd outside lockinternal/llama/installer.go (646 lines)llama.cpp binary download from GitHub releasesextractTarGzToBininternal/llama/gpu.go (232 lines)nvidia-smi errors silently ignored → 0 VRAM → CPU fallbackinternal/memory/store.go + retriever.go + embedder.gointernal/modelstore/modelstore.go (458 lines)internal/cloudsync/sync_manager.go + drive.go + crypto.gointernal/sessions/sessions.go (262 lines)internal/identity/identity.go + styles.gointernal/provider/ (10 files, ~1700 lines total)External LLM provider system:
provider.go — Provider interface (ChatCompletion, ChatCompletionStream, ListModels) + factoryconfig.go — ConfigManager: AES-256-GCM encrypted API keys in data/providers.json, machine-derived keyrouter.go — Multi-provider router with fallback chain, auto-disable after 3 failures, health check goroutineopenai.go — Full OpenAI-compatible implementation (ChatCompletion, SSE parsing, ListModels)gemini.go — Custom Google Gemini implementation (generateContent, streamGenerateContent)claude.go — Custom Anthropic Claude implementation (x-api-key auth, SSE event parsing)grok.go/groq.go/openrouter.go/ollama.go — Thin OpenAI-compatible wrappers (different base URLs)Priority field exists but unused by router; no test filesinternal/agent/ (8 files, ~1450 lines total)Agent execution engine:
tools.go — Tool definition system: ToolDef, DangerLevel (Safe/Medium/Dangerous), ToolRegistry with 8 built-in toolspermissions.go — PermissionManager: 6 policies (PromptAlways, AllowOnce/Session/Forever, DenyOnce/Forever), SHA-256 arg hashing, persistent JSON storagesandbox.go — Sandbox with path traversal protection, rate limiting (30 calls/min, 5s cooldown), command blacklist (23 patterns)pipeline.go — Pipeline: LLM → tool call → permission check → execution → result loop (max 20 iterations), event streaming via channelsexecutor.go — Top-level orchestrator: RunStream(), HandlePermissionResponse(), audit log (last 1000 entries)tools/file.go/command.go/search.go/calendar.go/provider.go/whatsapp.go/websearch.go/selfclone.go — 19 built-in tools total, with path validation, backup creation, environment variable maskingcommand: tools now execute through this same pipeline and permission UI (previously declaration-only)permissions_test.go, backup_test.go, tools/*_test.go); frontend agent UI is fully implemented (toggle in Chat's top bar)internal/orchestra/ (3 files, ~1000 lines total)Multi-model orchestration (Orchestra Mode):
conductor.go — Conductor: plan (chief analyzes + decomposes), execute (parallel/sequential tasks), synthesize (chief merges results)types.go — OrchestraConfig, OrchestraTask, OrchestraPlan, OrchestraResult, ProgressUpdate with streamingroles.go — 8 built-in roles (planner, frontend, backend, bug_fixer, reviewer, security, devops, general) with Turkish system promptscallWithRetry (rate limit + exponential backoff + jitter), retryTask (2 retries, 3s base delay)provider.Router (creates providers directly); chief model must support JSON outputinternal/api/types.go + client.go + streaming.go[DONE] chunk missing finish_reasongraph LR
subgraph Screens
CS[ChatScreen]
MS[ModelStoreScreen]
AS[AppShell]
end
subgraph Providers
CP[ChatProvider]
MP[ModelsProvider]
SP[SettingsProvider]
PP[ProviderListProvider]
OP[OrchestraConfigProvider]
end
subgraph Core
AC[ApiClient]
TH[Theme]
L10n[L10n]
end
subgraph Widgets
CML[ChatMessageList]
CI[ChatInput]
SD[SettingsDialog]
SW[SetupWizard]
MB[MessageBubble]
PC[ProviderConfigDialog]
OC[OrchestraConfigDialog]
end
AS --> CS & MS
CS --> CML & CI
CS --> CP
MS --> MP
AS --> SP
CP --> AC
MP --> AC
SP --> AC
PP --> AC
OP --> AC
CML --> MB
AC -->|HTTP/SSE| Backend
| Component | File | Lines | Responsibility |
|---|---|---|---|---|
| ApiClient | api_client.dart | ~600 | All REST + SSE communication |
| ChatProvider | chat_provider.dart | 192 | Message state, stream handling |
| ModelsProvider | models_provider.dart | 106 | Model list, download progress |
| SettingsProvider | settings_provider.dart | 270 | App settings, llama config |
| ProviderListNotifier | provider_provider.dart | 88 | Provider config CRUD |
| OrchestraConfigNotifier | orchestra_provider.dart | 31 | Orchestra config state |
| ChatMessageList | chat_message_list.dart | 450 | Message rendering, markdown |
| ChatInput | chat_input.dart | 283 | Text input, file attach, STT, /orchestra command |
| SettingsDialog | settings_dialog.dart | 2129 | 8-tab settings (incl. API Providers + Orchestra) |
| ProviderConfigDialog | provider_config_dialog.dart | 264 | Add/edit external provider config |
| OrchestraConfigDialog | orchestra_config_dialog.dart | 330 | Configure chief model + role assignments |
| ModelStoreScreen | model_store_screen.dart | 1223 | HF model search + download |
| SetupWizardView | setup_wizard_view.dart | 296 | First-run setup flow |
AnimationController per message bubble → severe jank with 50+ messages (fixed in v3.0.0)docs/DOCS.md §11 / BUG_REPORT.md for actively-maintained bug tracking (0 open as of 2026-08-05)This table predates several feature lines (Routines, Proactive Learning, Live Mode/TTS, Memo Swarm, Usage Stats, CLI providers, Skills, the Anthropic-compatible Developer Gateway) and is not exhaustive — see
docs/API_REFERENCE.mdfor those, orinternal/webserver/server.go'sroute(...)calls for the full current list (~118 endpoints as of v3.3.4).
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/send |
Classic message (JSON body) |
POST |
/api/send/stream |
Streaming message (SSE) |
POST |
/api/send_file |
File/image message (Multipart) |
GET |
/api/chats |
List all chat sessions |
POST |
/api/chats/new |
Create new chat |
POST |
/api/chats/switch |
Switch active session |
POST |
/api/chats/delete |
Delete session |
GET |
/api/messages |
Get active chat history |
GET |
/api/status |
System status + memory count |
POST |
/api/incognito |
Toggle incognito mode |
GET/PUT |
/api/system-prompt |
Get/update system prompt |
GET/DEL |
/api/memory/files |
List/delete memory files |
POST |
/api/memory/clear |
Clear all memory |
GET/DEL |
/api/models/local |
List/delete local models |
POST |
/api/models/start |
Start a model |
POST |
/api/models/stop |
Stop running model |
GET |
/api/models/status |
Model runtime status |
GET |
/api/gpu |
GPU detection info |
POST |
/api/models/search |
Search HuggingFace for GGUF |
POST |
/api/models/download |
Start model download |
GET |
/api/models/download/progress |
Download progress |
GET |
/api/models/llama/check |
Check llama.cpp installed |
GET/PUT |
/api/sync/settings |
Cloud sync settings |
GET/PUT |
/api/config/llama |
Llama configuration |
POST |
/api/image |
Read image (⚠️ arbitrary file read) |
POST |
/api/embed/start/stop |
Embedding server control |
GET/PUT/DELETE |
/api/providers |
List/update/delete provider configs |
POST |
/api/providers/test |
Test provider connection |
GET/PUT |
/api/providers/active |
Get/set active provider |
GET/PUT |
/api/agent/enabled |
Get/set agent mode |
POST |
/api/agent/permission |
Respond to permission request |
GET |
/api/agent/permissions |
List permanent permissions |
DELETE |
/api/agent/permissions |
Revoke or clear permissions |
GET/PUT |
/api/orchestra/config |
Get/update orchestra config |
# Terminal 1: Backend
go run . --port 8090
# Terminal 2: Frontend
cd frontend && flutter run -d linux
./scripts/package_linux.sh
# Output: build_output/memo-linux-x64/
Current version: v5.0.0 (2026-11-07).
Bug tracking: BUG_REPORT.md (repo root) is the actively-maintained log. docs/KNOWN_ISSUES.md and docs/RESOLVED_ISSUES.md are frozen historical snapshots from the v3.x line, kept for reference only.
Since v3.3.4, headline architectural additions (see versinNote/ for full per-version detail):
internal/livemode/ — Live Mode v2: native audio-to-audio voice (Google Live / OpenAI Realtime sessions), delegate/standalone modes, reconnect + transcript handling; internal/telegram/ — a second messaging bridge mirroring WhatsApp's shape.internal/taskloop/ (large expansion) — the Self-Driving task loop: Task.md schema, planner/executor engine, sub-agent orchestration, escalating retry, provider-lock hardening; internal/openaiapi/ — OpenAI-compatible sibling of the Developer Gateway; real tool-calling added for the Claude and Gemini providers; internal/geminisub/ — sign in with a personal Google account, reach Gemini via Code Assist (Beta); internal/remoteauth/ split out of the webserver package (bruteforce lockout, per-device tokens, JWT session tokens); internal/browserengine/ — optional headless-browser rendering for JS-heavy pages internal/websearch alone can't read.frontend/lib/mascot_main.dart + mascot_window.dart/memo_mascot.dart/mascot_provider.dart, a second window in the same process, driven by an app-wide activity signal in internal/app/activity.go/internal/models/activity.go); Code Mode's Plan/Auto/Build sub-modes (Session.CodeSubMode, Pipeline.codeSubMode, the save_code_plan tool); both Developer Gateway endpoints now key-enforced for non-loopback callers; imported skills no longer auto-activate.internal/cliproxy/ — the Subscriptions sidecar: a bundled, SHA-256-pinned CLIProxyAPI child process on loopback that turns an Antigravity / Claude / Codex sign-in into one OpenAI-compatible endpoint (internal/app/subs.go registers it as the Subscriptions custom provider; internal/geminisub/ and internal/claudesub/ from the earlier Beta line are deleted); per-model quota (internal/cliproxy/quota*.go) feeding the model picker and the quota_exhausted/quota_low stream markers (Server.withQuotaSignals, internal/app/quotasignal.go); image generation/editing for custom and OpenRouter providers (internal/provider/*_images.go); open_app and the interactive browser Session (internal/browserengine/session.go, browser_* tools, browser_frame SSE chunks, /api/browser/session/*); plain-chat streams ended by silence not length (internal/app/stream_watchdog.go) with heartbeat chunks; prompt-cache accounting across providers (provider.Usage, internal/stats/); per-chat skill activation; the Cline provider; Android/iOS targets of the single Flutter client (the mobile/ project is retired); internal/upstream/ and the scheduled upstream.yml watch of everything outside this repo that Memo depends on. Security: destructive and credential-bearing routes are now explicitly gated (adminOnly/adminWrites/redactSecrets), file:// is refused by the browser session, incognito is honoured on chat-addressed sends.internal/updater/, internal/components/, cmd/memo-updater/, cmd/memo-sign/ — in-app updates: ed25519-signed manifests, byte-exact resumable downloads, an in-place swap with rollback, and separate engine updates; internal/webchat/ — ChatGPT / claude.ai through a real browser window (Beta).internal/mail/, internal/twin/, internal/autonomy/ — the mail engine, the Digital Twin store and the safety/decision layer (see the data flow below and DIGITAL_TWIN_AND_MAIL.md). The daily briefing is built by autonomy.Briefer, kept in twin.db, and delivered by App.deliverMorningBriefings through a claim-before-send ledger (briefing_deliveries).internal/project/ (project folders with their own memory scope, memories.project_id), internal/subagents/ (chat sub-agents), skills called by name, the streamed agent loop, one chat list with a Chat | Agent start screen.internal/telemetry/ — the anonymous daily summary (on by default in official builds).internal/llama/device_probe.go), a Vulkan backend plugin for prompt processing and n-gram speculative decoding (internal/llama/spec.go).frontend/lib/widgets/setup_wizard_view.dart, frontend/lib/widgets/setup/), whose progress is a pure function of real app state.logx.Recover/logx.GoRecover wrap essentially every background goroutine, so an unexpected error in one no longer takes the whole process down.Package count has grown to 65 (ls internal/) and registered routes to 262 (each also served under /api/v1/) (internal/webserver/server.go's route(...) calls) — the endpoint table in §6 above predates most of this and is illustrative, not exhaustive; see docs/API_REFERENCE.md for the maintained list.
sequenceDiagram
participant W as mail.Watcher (60 s poll)
participant S as autonomy shield
participant G as mail.Gatekeeper
participant T as twin.Store
participant E as autonomy engine
participant A as approval bridge
participant O as owner (app / WhatsApp / Telegram)
W->>S: new message (after the high-water mark only)
S-->>W: blocked? kept in inbox, counted by reason code, no model reads it
W->>G: Evaluate (contacts + structure, headers, model)
G-->>W: category (bulk and one-time-code mail is dropped from memory)
W->>T: record message under the linked contact
W->>E: prepare (read-only research → draft in your style)
E->>E: outbound DLP, independent score
E->>A: pending action (Drafts folder + review screen)
A->>O: notification (ids and states only)
O-->>A: approve / cancel / never (or the undo window runs out)
A->>A: re-check every rule, claim the send once
A-->>W: SMTP send, recorded in the twin
Last updated: 2026-11-07
Version: v5.0.0