` tags from accumulated response
- Single `thinking` event sent to frontend with extracted content
- Zero mid-stream parsing complexity, works reliably
- **Markdown content overflow protection** (Updated 2025-10-11)
- **CSS constraints**: `.markdown-content` class with `max-width: 100%`, `overflow-wrap: break-word`, `word-break: break-word`
- **Flex container fix**: Added `min-w-0` to assistant message flex container (line 418) to enable proper shrinking
- **Code block handling**: `overflow-x: auto` on `` tags for horizontal scrolling when needed
- Prevents markdown content (especially malformed code blocks) from extending beyond screen width
- **Implementation**: `index.css:55-70` (CSS rules), `TerminalMessageThread.tsx:418` (flex constraint)
- **Memory citations display** (Updated 2025-10-08)
- **UI Display**: Bottom-right metadata placement (only after streaming complete)
- Collapsible block showing "X memories" used
- Expands to show: collection name (color-coded), full text preview
- **Note**: Confidence scores are NOT displayed in UI (removed 2025-10-07 due to unreliable metrics)
- **Backend Only**: System calculates confidence using exponential decay `exp(-distance / 100.0)` for internal learning (outcome detection, memory promotion)
- Color-coded collections: purple (books), blue (working), green (history), yellow (patterns), pink (memory_bank)
- **Implementation**: [TerminalMessageThread.tsx:10-59](../ui-implementation/src/components/TerminalMessageThread.tsx#L10-L59) - CitationsBlock component
### First-Run Experience
**Welcome modal (v0.3.1+):** `OllamaRequiredModal.tsx` explains the chat-model + sidecar-model architecture, surfaces a curated tool-capable model list (qwen3, gemma4, gpt-oss), and treats LM Studio as a first-class provider. After the first chat-model install a follow-up toast prompts the user to pick a sidecar.
**Zero-Model onboarding behavior:**
- **Graceful startup**: backend starts even with no models installed
- **Empty State UI**: prominent centered message when no models are available
- **Clear CTA**: large "Install Your First Model" button in the chat area
- **Embedding model filter**: dropdown excludes embedding-only models (`nomic-embed`, `llava`, `bge-`, `all-minilm`, `mxbai-embed`) — `ConnectedChat.tsx`, `getModelOptions()`
- **Smart first-model auto-switch**: after downloading the first chat model, polls `/api/model/current`; if backend has no chat model (embedding-only or none), auto-switches to the newly installed model. Subsequent installs show a success toast only; user switches manually.
- **Lazy model loading**: Models load into GPU memory only on first user message, not during app startup or model switching. Health checks verify model availability without loading. Sidecar operations use client locking to prevent concurrent model loading and retry queue for reliability.
- **Auto-selection behavior**: If no model is configured in `.env`, app selects first available model from detected provider. **Recommendation**: Configure `OLLAMA_MODEL` or `ROAMPAL_LLM_OLLAMA_MODEL` in `.env` to avoid auto-selection of large models.
#### Model Selection Synchronization
**Backend as Source of Truth:**
- **Initial sync on mount**: UI syncs with backend on startup via `/api/model/current` ([ConnectedChat.tsx:246-250](../ui-implementation/src/components/ConnectedChat.tsx#L246))
- **Auto-sync after model list changes** ([ConnectedChat.tsx:187-188](../ui-implementation/src/components/ConnectedChat.tsx#L187)): `fetchModels()` calls `fetchCurrentModel()` after updating available models, ensuring UI syncs when backend auto-switches providers (e.g., after uninstalling active model)
- **fetchCurrentModel()** ([ConnectedChat.tsx:268-308](../ui-implementation/src/components/ConnectedChat.tsx#L268)):
- Fetches backend's active model via `/api/model/current`
- Backend returns `current_model`, `provider`, `can_chat`, `is_embedding_model` flags
- Updates UI `selectedModel`, `selectedProvider`, and localStorage to match backend
- Handles case where backend has no chat model available (switches to first available)
- Prevents phantom selection where UI shows different model than backend is using
**Installation Auto-Switch Logic:**
- **First chat model**: Auto-switches when `can_chat: false` or `is_embedding_model: true` on backend
- **Subsequent models**: Shows "✓ model installed successfully" toast, requires manual switch
- **Rationale**: Unblocks user on first install, preserves workflow control on subsequent installs
**Flow:**
1. User launches Roampal.exe for first time
2. UI detects no models available
3. Chat area shows empty state with install prompt
4. User clicks button to open Model Manager
5. User browses and downloads preferred model(s)
6. System ready to chat
**Implementation:**
- Backend: [main.py:243-245](../main.py) - Allows startup without models, logs setup mode
- UI Empty State: [ConnectedChat.tsx:1502-1529](../ui-implementation/src/components/ConnectedChat.tsx) - Centered prompt with icon, message, and install button
- Dropdown Filter: [ConnectedChat.tsx:1343](../ui-implementation/src/components/ConnectedChat.tsx) - Uses `getModelOptions()` to exclude embedding models
- Dropdown Text: [ConnectedChat.tsx:1384](../ui-implementation/src/components/ConnectedChat.tsx) - Shows "Download Your First Model" when empty
### Personality Customization
Users can customize the AI's personality and response style while preserving core memory functionality:
**Customizable Elements:**
- **Identity & Name** - Customize assistant name (appears in UI sidebar and chat messages)
- **Communication Style** - Tone, verbosity, formality with visual icons
- **Response Behavior** - Citation style, clarification approach, reasoning visibility
- **Memory Usage** - Reference frequency and pattern trust levels
- **Personality Traits** - Custom traits list (helpful, direct, patient, etc.)
- **Custom Instructions** - Free-form additional guidelines
**Base Templates:**
- `default.txt` - Balanced, memory-enhanced assistant (Recommended)
- `professional.txt` - Concise, direct, business-focused
- `teacher.txt` - Detailed, patient, educational
**File Format:** YAML-based templates stored in `backend/templates/personality/`
**User Interface (Two Modes):**
- **Quick Settings** (Default) - 4 essential fields with dropdowns and clear labels
- Assistant Name (text field)
- Conversation Style (warm/professional/direct/enthusiastic)
- Response Length (concise/balanced/detailed)
- Memory References (when relevant/frequently)
- Custom Instructions (optional textarea)
- **Advanced Mode** - Full YAML editor with:
- Real-time validation (using js-yaml library)
- Friendly error messages
- Inline comments showing available options
- Load example template button
- Syntax highlighting
**User Capabilities:**
- Select from preset templates
- Edit in Quick Settings (beginner-friendly) OR Advanced mode (full control)
- Export personality configuration to file
- Changes apply immediately (no restart required)
- Unsaved changes warning with confirmation dialogs
- Reset to last saved version
**System-Protected Elements (Hardcoded):**
- Memory context injection logic
- Collection reliability labels (✓ PROVEN SOLUTION, 📚 Reference docs, etc.)
- Core instructions for using memory system
- Prompt structure order
- Memory variable injection points
**Technical Flow:**
```
1. User selects/edits personality template in Quick Settings or Advanced mode
2. Frontend validates YAML in real-time (js-yaml library)
3. On save: Backend validates and saves to backend/templates/personality/active.txt
4. agent_chat.py loads template on each message (cached with mtime checking)
5. Template converted to natural language and prepended to prompt
6. Memory context auto-injected after personality layer
7. LLM receives: [Personality] + [Core Instructions] + [Memory] + [History] + [Question]
8. UI components fetch assistant name from identity.name field:
- Sidebar displays custom name above conversations (polls every 5s)
- Chat messages show custom name instead of "RoamPal"
```
**Prompt Structure:**
```
[PERSONALITY LAYER] ← User-customizable
{{ personality_template }}
[CORE MEMORY INSTRUCTIONS] ← Hardcoded (system-critical)
{{ memory_usage_guidelines }}
[MEMORY CONTEXT] ← Auto-injected by Roampal
{{ memory_results with collection labels }}
[CONVERSATION HISTORY] ← Auto-injected by Roampal
{{ recent_messages }}
[CURRENT QUESTION] ← Auto-injected by Roampal
{{ user_input }}
```
**Key Implementation Files:**
- **Backend:**
- `app/routers/personality_manager.py` - API endpoints for CRUD operations
- `app/routers/agent_chat.py` - Template loading and caching (lines 121-124, 376-491)
- `backend/templates/personality/active.txt` - Currently active template
- `backend/templates/personality/presets/` - Built-in presets (default, professional, teacher)
- **Frontend:**
- `ui-implementation/src/components/PersonalityCustomizer.tsx` - Main UI component
- `ui-implementation/src/components/Sidebar.tsx` - Displays custom name (line 88-113)
- `ui-implementation/src/components/EnhancedChatMessage.tsx` - Shows name in messages (line 28-50)
**UX Best Practices Applied:**
- Clear terminology ("Quick Settings" vs "Advanced", not "Simple" vs "Guided")
- Progressive disclosure (info panel toggleable, advanced mode optional)
- Unsaved changes protection with confirmation dialogs
- Real-time validation with friendly error messages
- Visual feedback (amber dot for unsaved, checkmark on success)
- Consistent zinc color palette matching rest of UI
- Heroicons instead of emojis for professional appearance
This architecture ensures users can fully customize personality while preserving Roampal's core memory intelligence.
### Processing Transparency
When the AI is processing queries, it shows real-time status:
- Global processing indicator (no prefix, clean display)
- Real-time status updates via WebSocket ("Processing...", "Searching memory...", etc.)
- Intent-based messages when no explicit status provided
- Clean, single-purpose display (no redundant components)
- Automatically hides when response appears
### Session Management
- Create/switch conversations
- Automatic inline title generation (after first exchange, single LLM call)
- **Delete conversations** with hover-activated trash button
- Subtle trash icon appears on hover over conversation items
- Confirmation dialog prevents accidental deletion
- Backend prevents deletion of active conversation (400 error)
- Memory cleanup removes all ChromaDB entries for deleted conversation
- Session list auto-refreshes after deletion
- Conversation history with atomic file writes
- Memory context preservation
- Session files protected with FileLock + fsync for power-loss safety
## Security Features
### Development Mode
- No authentication required
- All localhost origins allowed
- Debug logging enabled
- Rate limiting: 100 req/min (always enabled)
### Production Mode
- API key authentication
- IP whitelisting (localhost only)
- Rate limiting: 100 req/min (always enabled)
- Sanitized logging
- CORS restrictions
### Desktop App Process Lifecycle (v0.2.3)
The Tauri desktop app manages the Python backend process with proper cleanup on window close.
**Implementation:** [main.rs:410-424](../ui-implementation/src-tauri/src/main.rs#L410-L424)
**Process Management:**
```rust
// Backend process is tracked in shared state
struct BackendProcess(Arc>>);
// Window close handler kills the backend
main_window.on_window_event(move |event| {
match event {
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => {
if let Ok(mut backend) = backend_clone.lock() {
if let Some(mut child) = backend.take() {
let _ = child.kill();
let _ = child.wait(); // Wait for full termination
}
}
}
_ => {}
}
});
```
**Key Events Handled:**
| Event | Description |
|-------|-------------|
| `CloseRequested` | User clicks X button - kills backend immediately |
| `Destroyed` | Window destroyed - fallback cleanup |
**Why Both Events?**
- `CloseRequested` fires first when user clicks X (before window closes)
- `Destroyed` fires after window is already gone (backup handler)
- Using both ensures backend is killed in all close scenarios
**Previous Issue (Fixed v0.2.3):**
- Only handled `Destroyed` event, not `CloseRequested`
- Backend processes became orphaned when closing via X button
- Users had to manually kill Python processes via Task Manager
## Performance Optimization
### Caching
- Concept extraction cached
- Embeddings reused when possible
- Knowledge graph paths cached
### Bounded Collections
- Working memory: Max 100 items
- Memory bank: Max 500 items (`MemoryBankService.MAX_ITEMS = 500`)
- History per conversation: 20 messages
- LLM context: Last 4 exchanges (8 messages)
### Lazy Loading
- Memory search on-demand
- Metrics collected but not exported by default
## Learning System Details
### Outcome Detection Integration
Outcome detection is integrated into the streaming chat flow:
1. When user sends a message, system checks if previous message was from assistant
2. If yes, analyzes [previous assistant response, current user message] for outcome
3. Scores the previous exchange in memory based on detected outcome
4. Memory items are promoted/deleted based on accumulated scores
### How Roampal Learns
1. **Concept Extraction**: Identifies key terms and patterns from queries
2. **Usage Tracking**: Records which memories were helpful
3. **Outcome Recording**: Detects success/failure from conversation
4. **Adaptive Routing**: Learns best memory collections for concepts
5. **Pattern Recognition**: Identifies recurring problem→solution pairs
### Example Learning Cycle
```
User: "How do I fix authentication errors?"
Roampal: [Searches all collections] "Try checking your API key..."
User: "That worked, thanks!"
System: Records success → Links "authentication" to solution →
Updates routing to prioritize this pattern
Next query about authentication → Faster, more accurate response
```
## Troubleshooting
### Memory not learning?
1. Check `ROAMPAL_ENABLE_OUTCOME_TRACKING=true`
2. Verify clear success/failure signals in conversation
3. Check `outcomes.db` is being written
### Slow responses?
1. Check Ollama is running (`http://localhost:11434`)
2. Verify ChromaDB performance
3. Review memory collection sizes
4. Check system resources
### High memory usage?
1. Check conversation count in cache
2. Review working memory size
3. Run memory cleanup/promotion
4. Restart to clear caches
## Architecture Decisions
### ADR-001: Single Memory System
**Decision**: Use UnifiedMemorySystem as the single source of truth
**Rationale**: Eliminates complexity, ensures consistency
**Impact**: All features integrate through one system
### ADR-002: Memory-First Design
**Decision**: Build all features on top of memory system
**Rationale**: Memory is the core value proposition
**Impact**: Stable foundation for all enhancements
### ADR-003: Local-First Storage
**Decision**: All data stored locally, no cloud dependencies
**Rationale**: Privacy, performance, offline capability
**Impact**: Full data ownership for users
### ADR-004: Learn from Interaction
**Decision**: No separate training, learn during conversations
**Rationale**: Continuous improvement, personalized learning
**Impact**: System improves with use
### ADR-005: Transparent AI
**Decision**: Show thinking process when helpful
**Rationale**: Build trust through transparency
**Impact**: Users understand AI reasoning
### ADR-006: Security-First Book Processing
**Decision**: Implement comprehensive input validation and security checks
**Rationale**: Single-user local app still needs protection from accidental corruption
**Impact**: Prevents data integrity issues and self-inflicted prompt injection
**Implementation**: File type whitelisting, size limits, encoding validation, UUID validation, metadata length limits
## Security
**⚠️ IMPORTANT: This is a single-user local application. It is NOT designed for multi-user or public internet deployment without additional security measures.**
### Security Measures (Current)
**1. Data Privacy** ✅
- All data stored locally in `/data` directory
- No cloud sync or external data transmission
- API keys stored in `.env` (gitignored)
- User conversations and memories never leave your machine
**2. CORS Protection** ✅
- Restricted to `localhost` origins only (Tauri + dev servers)
- No wildcard (`*`) origins allowed
- Prevents external websites from accessing your local API
- Location: `main.py:232` - `allow_origins` from env var
**3. Input Validation** ✅
- Message length limit: 10,000 characters (configurable via `ROAMPAL_MAX_MESSAGE_LENGTH`)
- Conversation size limit: 1000 messages per session (prevents OOM crashes)
- Control character removal (prevents injection attacks)
- Empty message rejection
- Location: `app/routers/agent_chat.py:58-70, 207-210, 195-197`
**4. File Upload Security** ✅
- File size limit: 10MB maximum for books
- File type restriction: Only `.txt` and `.md` allowed
- UTF-8 encoding validation (rejects binary/malformed files)
- Duplicate detection to prevent storage abuse
- Prompt injection sanitization: Removes malicious patterns ([IGNORE], [SYSTEM], <|im_start|>, etc.)
- Location: `backend/api/book_upload_api.py:44-125`, `app/routers/agent_chat.py:206-218`
**5. Path Traversal Protection** ✅
- UUID validation for book_id and session_id (prevents `../../` attacks)
- Files scoped to `/data/books/` and `/data/sessions/` directories
- Regex validation on all ID parameters
- Path resolution checking with relative_to() validation
- Location: `backend/api/book_upload_api.py:215-220`, `app/routers/sessions.py:116-123`
**6. Concurrency Protection** ✅
- Per-session async locks prevent race conditions in file writes
- Atomic writes to session JSONL files
- Thread-safe conversation history management
- Location: `app/routers/agent_chat.py:110-117`
**7. GitIgnore Protection** ✅
- `.env` files excluded (API keys protected)
- `/data` directory excluded (user data protected)
- Log files excluded (prevents info leakage)
- Session files excluded (conversation privacy)
- Location: `.gitignore`
### Security Limitations (By Design for Single-User)
**Not Implemented** ❌
- No user authentication (not needed for localhost single-user)
- No rate limiting (single user can't DoS themselves)
- No API key rotation (manual management only)
- No HTTPS/TLS (localhost uses HTTP)
- No request/response audit logging (privacy-first design)
### For Open Source Contributors
**If you're forking this project:**
1. **API Keys**: Never commit `.env` files - use `.env.example` as template
2. **User Data**: Never commit `/data` directory - it contains personal information
3. **Security Model**: This is designed for localhost single-user use
4. **Production Deployment**: If deploying publicly, you MUST add:
- User authentication (JWT/session tokens)
- Rate limiting (prevent API abuse)
- HTTPS/TLS encryption
- Input sanitization review
- Security audit
### Threat Model
**Protected Against** ✅
- Path traversal attacks (UUID validation, path resolution checking)
- Control character injection (XSS prevention)
- Prompt injection attacks (regex-based sanitization)
- File upload abuse (size/type limits: 10MB books via Document Processor)
- DoS via unbounded growth (1000 message limit per conversation)
- Race conditions (per-session async locks)
- External CORS attacks (localhost-only)
- Accidental credential leaks (gitignore)
**Not Protected Against** ⚠️
- Malicious localhost applications (same-origin access)
- Physical machine access (no encryption at rest)
- OS-level vulnerabilities (depends on system security)
- Social engineering (user must protect API keys)
### Best Practices for Users
1. **API Key Security**
- Store OpenAI/Anthropic keys in `.env` only
- Never share your `.env` file
- Rotate keys if compromised
2. **Data Backups**
- Export data regularly (Settings → Export Data)
- Back up `/data` directory before major updates
- Keep backups offline for privacy
3. **System Security**
- Run Roampal on trusted local machine only
- Keep OS and dependencies updated
- Use firewall to block external access to port 8000
4. **For Developers**
- Review security settings in `.env.example`
- Test file uploads with edge cases (large files, weird encodings)
- Never disable CORS restrictions
## Out-of-Memory graceful degradation
When a model is configured with a context window too large for the host's RAM/VRAM, Ollama terminates the runner with `"llama runner process has terminated: exit status 2"`. The chat path handles this in three layers:
1. **Lightweight health check during model switch.** `model_switcher.py` validates a new model with `num_ctx=2048, num_predict=10`, so the switch itself succeeds even on weak hardware.
2. **Auto-retry on OOM during chat.** `ollama_client.py` detects the `terminated` error in 500 responses, retries the same request with `num_ctx=2048`, and tags the payload `_oom_recovered=True`.
3. **User-facing warning.** The recovered response is prefixed with a "Memory limit reached" banner that names the original context size and points at Settings → Context Window Settings → lower context for the offending model.
Strong-hardware users never see this path. Weak-hardware users get a working response on the first message and a one-click permanent fix.
---
## Per-release implementation notes
Historical per-release implementation breakdowns are maintained in `dev/docs/releases/vX.Y.Z/RELEASE_NOTES.md`:
- v0.3.0 — `releases/v0.3.0/RELEASE_NOTES.md` (duplicate-prompt race, data deletion toasts, ChromaDB 1.x migration safety)
- v0.3.1 — `releases/v0.3.1/RELEASE_NOTES.md` (KG removed, TagCascade landed, ONNX embeddings)
- v0.3.2 — `releases/v0.3.2/RELEASE_NOTES.md` (TagCascade `$contains` repair, chat-path latency, customer bug batch)
- v0.3.3 — `releases/v0.3.3/RELEASE_NOTES.md` (multimodal images, dynamic capability detection, issue-#8 closure, 21 defects)
This document describes the current state of the system, not the history of how it got there.
---
## License
Apache 2.0 — see `LICENSE` for details.