Web UI
robotmem includes a built-in web dashboard for browsing, searching, and managing memories. It runs as a Flask application with 12 REST API endpoints.
Starting the Web UI
python -m robotmem web
# Server starts at http://localhost:6889
The web UI shares the same SQLite database as the MCP server.
Architecture
python -m robotmem web
│
┌────▼────────────────────────┐
│ Flask App Factory │
│ create_app() │
│ │
│ ┌────────────────────────┐ │
│ │ api_bp (Blueprint) │ │
│ │ /api/* (12 routes) │ │
│ └────────────────────────┘ │
│ │
│ GET / → index.html │
│ CogDatabase (shared conn) │
└─────────────────────────────┘
The Flask app uses the app factory pattern (create_app()):
- Creates a
CogDatabaseinstance with shared SQLite connection - Registers the API blueprint at
/api - Serves the frontend at
/
The database and config are stored in app.config:
app.config["ROBOTMEM_DB"] = db
app.config["ROBOTMEM_CONFIG"] = config
REST API Endpoints
Health & Statistics
GET /api/doctor
Health check endpoint — verifies database integrity and reports system status.
Response:
{
"memories": {
"total": 42,
"by_type": {"fact": 35, "perception": 7}
},
"sessions": {
"total": 8,
"by_status": {"active": 1, "ended": 7}
},
"fts5": {
"indexed": 42,
"expected": 42,
"ok": true
},
"vec0": {
"indexed": 40,
"expected": 42,
"ok": false
},
"zero_hit": {
"count": 5,
"total": 42,
"rate": 11.9
},
"db_size_bytes": 524288
}
| Field | Description |
|---|---|
memories.total |
Active memories count |
fts5.ok |
FTS5 index is in sync with memories table |
vec0.ok |
vec0 index is in sync with memories table |
zero_hit.rate |
Percentage of memories never recalled (potential cleanup candidates) |
db_size_bytes |
Database file size on disk |
GET /api/stats
Aggregate statistics.
Response:
{
"total": 42,
"by_type": {"fact": 35, "perception": 7},
"by_category": {"observation": 20, "pattern": 8, "decision": 5, "constraint": 3},
"collections": ["default", "grasping", "navigation"],
"recent_24h": 3
}
Memory Operations
GET /api/memories
Paginated memory listing with filters.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int | 0 | Page number (0-indexed) |
limit |
int | 30 | Items per page (1-100) |
collection |
string | — | Filter by collection |
type |
string | — | Filter by type (fact or perception) |
status |
string | active |
Filter by status |
category |
string | — | Filter by category (comma-separated for multiple) |
confidence_min |
float | — | Minimum confidence threshold |
confidence_max |
float | — | Maximum confidence threshold |
days |
int | — | Only memories from last N days |
perception_type |
string | — | Filter by perception type |
Response:
{
"memories": [
{
"id": 1,
"collection": "default",
"type": "fact",
"content": "grip_force=12.5N optimal for cylindrical objects",
"human_summary": "grip_force=12.5N optimal...",
"perception_type": null,
"category": "observation",
"confidence": 0.85,
"decay_rate": 0.01,
"source": "tool",
"scope": "project",
"status": "active",
"access_count": 5,
"created_at": "2026-03-09T10:30:00",
"updated_at": "2026-03-09T10:30:00"
}
],
"total": 42,
"page": 0,
"limit": 30,
"pages": 2
}
GET /api/search?q=
Full-text search across memories using FTS5 BM25 ranking.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string | (required) | Search query |
collection |
string | — | Limit to collection (empty = all collections) |
top_k |
int | 10 | Maximum results (1-50) |
Response:
{
"results": [
{
"id": 1,
"content": "grip_force=12.5N optimal for cylindrical objects",
"type": "fact",
"category": "observation",
"confidence": 0.85,
"bm25_score": -3.45,
"params": {"grip_force": {"value": 12.5, "unit": "N"}},
"spatial": {"object_position": [1.3, 0.7, 0.42]},
"created_at": "2026-03-09T10:30:00"
}
],
"total": 1,
"query": "grip force"
}
When no collection is specified, the search runs across all collections and merges results by BM25 score.
Context fields (params, spatial, robot, task) are automatically extracted from the context JSON for convenience.
GET /api/memory/:id
Get full details for a single memory.
Response: Complete memory object (all fields from memories table, excluding embedding binary).
PUT /api/memory/:id
Update memory fields.
Request Body (JSON):
{
"content": "updated content text",
"confidence": 0.90,
"category": "pattern"
}
Allowed fields: content, human_summary, category, confidence, decay_rate, scope, context
Response:
{
"status": "updated",
"memory_id": 1,
"fields": ["content", "confidence"]
}
DELETE /api/memory/:id
Soft-delete a memory (sets status to invalidated).
Request Body (JSON):
{
"reason": "Incorrect sensor reading"
}
If no reason is provided, defaults to "Web UI delete".
Response:
{
"status": "deleted",
"memory_id": 1
}
Session Operations
GET /api/sessions
Paginated session listing with memory counts.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int | 0 | Page number |
limit |
int | 20 | Items per page (1-50) |
Response:
{
"sessions": [
{
"id": 1,
"external_id": "550e8400-e29b-41d4-a716-446655440000",
"collection": "default",
"context": "{\"robot_id\": \"arm-01\"}",
"session_count": 1,
"status": "ended",
"created_at": "2026-03-09T10:00:00",
"updated_at": "2026-03-09T11:00:00",
"memory_count": 15
}
],
"total": 8,
"page": 0,
"limit": 20
}
The memory_count field shows how many active memories belong to each session (via LEFT JOIN).
GET /api/sessions/:external_id/memories
List all memories within a session, ordered chronologically (timeline view).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 20 | Maximum results (1-100) |
Response:
{
"memories": [
{
"id": 1,
"type": "fact",
"content": "Starting calibration...",
"human_summary": "Starting calibration...",
"perception_type": null,
"category": "observation",
"confidence": 0.85,
"created_at": "2026-03-09T10:05:00"
}
],
"total": 15
}
Results are ordered by created_at ASC for chronological timeline viewing.
Metadata Operations
GET /api/collections
List all collections with memory counts.
Response:
{
"collections": [
{"name": "default", "count": 30},
{"name": "grasping", "count": 10},
{"name": "navigation", "count": 2}
]
}
GET /api/categories
List all categories present in the database with counts.
Response:
{
"categories": [
{"name": "observation", "count": 20},
{"name": "pattern", "count": 8},
{"name": "decision", "count": 5},
{"name": "constraint", "count": 3}
]
}
GET /api/recent-failures
Recent postmortem and gotcha memories — useful for reviewing recent lessons learned.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 5 | Maximum results (1-20) |
Response:
{
"failures": [
{
"id": 10,
"collection": "default",
"type": "fact",
"content": "Lesson: always calibrate before new session",
"human_summary": "Always calibrate before new session",
"perception_type": null,
"category": "postmortem",
"confidence": 0.95,
"created_at": "2026-03-09T10:30:00"
}
],
"total": 1
}
API Summary
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/doctor |
Health check: FTS5/vec0 sync, zero-hit rate, DB size |
| GET | /api/stats |
Total counts, type/category distribution, collections |
| GET | /api/memories |
Paginated list with filters |
| GET | /api/search?q= |
FTS5 full-text search |
| GET | /api/memory/:id |
Single memory detail |
| PUT | /api/memory/:id |
Update memory fields |
| DELETE | /api/memory/:id |
Soft-delete memory |
| GET | /api/sessions |
Paginated session list with memory counts |
| GET | /api/sessions/:id/memories |
Memories within a session (timeline) |
| GET | /api/collections |
Collection list with counts |
| GET | /api/categories |
Category list with counts |
| GET | /api/recent-failures |
Recent postmortem/gotcha memories |
Three-Layer Defense
All API endpoints follow the same defense pattern:
| Layer | Phase | Implementation |
|---|---|---|
| L1 | Before | Parameter validation (type, range, non-empty) |
| L2 | During | try-except wrapping all DB operations |
| L3 | After | Unified JSON response + error codes |
Error Responses
All errors return JSON with an error field:
// 400 Bad Request
{"error": "q parameter cannot be empty"}
// 404 Not Found
{"error": "Memory #42 does not exist"}
// 500 Internal Server Error
{"error": "Query failed"}
The server never returns raw exception traces to the client.
Configuration
The web UI shares configuration with the MCP server. Key settings:
| Setting | Default | Description |
|---|---|---|
| Port | 6889 | HTTP server port |
| Database | ~/.robotmem/memory.db |
Same database as MCP server |
ROBOTMEM_HOME |
~/.robotmem |
Override config/database directory |
Custom Port
Currently the port is hardcoded to 6889 in __main__.py. To change it, modify the Flask run command.
Shared Database
The Web UI and MCP server can run simultaneously — SQLite WAL mode supports concurrent readers. However, only one writer can operate at a time (5-second busy timeout).