# Message Architecture & Context Design This document describes the new message management and context processing architecture that replaced the legacy SharedState system. The new design follows clean architecture principles with a single source of truth, computed views, and complete project isolation. **Note**: For detailed information about the layered context system (L1-L5 layers, L2 auto-promotion, cache optimization), see [CONTEXT_ENGINEERING.md](./CONTEXT_ENGINEERING.md). ## Architecture Principles ### Single Source of Truth - Each message is stored exactly once in `MessageRepository` - All UI and LLM views are computed from this single storage - No complex dual-array synchronization or ID matching ### Clean Architecture Flow ``` User Input → ChatUIState → ChatManager → getCurrentMessageRepo() → MessageRepository + ContextManager ↓ ↓ ChatPersistenceManager Project-specific storage ↓ UI Components ← ChatUIState ← Computed Views ← MessageRepository ↓ LLM Processing ← Chain Memory ← getLLMMessages() ← MessageRepository ``` ### Context Always Fresh - Context is reprocessed when messages are edited - No stale context issues from cached processing - Ensures accurate context for LLM interactions ### Project Isolation - Each project maintains its own isolated chat history - Automatic detection and switching when project changes - Zero configuration required - works automatically - Non-project chats use a default repository ## Core Components ### 1. MessageRepository (`src/core/MessageRepository.ts`) **Purpose**: Single source of truth for all messages **Key Concepts**: - Stores `StoredMessage` objects with both `displayText` and `processedText` - `displayText`: What the user typed or AI responded (for UI display) - `processedText`: For user messages, includes context. For AI messages, same as display **Core Methods**: ```typescript // Add new message addMessage( displayText: string, processedText: string, sender: string, context?: MessageContext, content?: MessageContent[] ): string // Get computed views getDisplayMessages(): ChatMessage[] // For UI rendering getLLMMessages(): ChatMessage[] // For AI processing // Edit operations editMessage(id: string, newDisplayText: string): boolean updateProcessedText( id: string, processedText: string, contextEnvelope?: PromptContextEnvelope ): boolean // Bulk operations truncateAfterMessageId(messageId: string): void loadMessages(messages: ChatMessage[]): void ``` > **Storing envelopes**: When `ChatManager` calls `addMessage` with a full `NewChatMessage`, it includes the `contextEnvelope` property so the repository keeps both the legacy `processedText` and the canonical layered representation. The string-based overload remains for low-level utilities and test fixtures. ### 2. ChatManager (`src/core/ChatManager.ts`) **Purpose**: Central business logic coordinator **Responsibilities**: - Orchestrates MessageRepository, ContextManager, and LLM operations - Handles all message CRUD operations with proper error handling - Synchronizes with chain memory for conversation history - Manages context processing lifecycle - **Project Isolation**: Maintains separate MessageRepository per project - **Persistence**: Integrates with ChatPersistenceManager for saving/loading **Envelope Lifecycle**: 1. User sends a message → `ContextManager.processMessageContext()` returns both `processedContent` **and** a `PromptContextEnvelope`. 2. `ChatManager` stores the envelope with `MessageRepository.updateProcessedText(...)`. 3. When any chain runner executes, it reads `userMessage.contextEnvelope` and feeds it to `LayerToMessagesConverter.convert()` to materialize the L1-L5 prompt structure. 4. Regeneration or edits call `reprocessMessageContext`, ensuring a fresh envelope replaces the stale one. **Key Operations**: ```typescript // Send new message with context processing async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise // Edit message and reprocess context async editMessage(messageId: string, newText: string, chainType: ChainType, includeActiveNote?: boolean): Promise // Regenerate AI response async regenerateMessage(messageId: string, onUpdateMessage: Function, onAddMessage: Function): Promise // Memory synchronization private async updateChainMemory(): Promise // Project management private getCurrentMessageRepo(): MessageRepository // Auto-detects current project async handleProjectSwitch(): Promise // Forces project detection // Persistence async saveChat(modelKey: string): Promise<{ success: boolean; path?: string; error?: string }> ``` **Project Isolation Implementation**: ```typescript // Internal structure private projectMessageRepos: Map // Automatic project detection getCurrentMessageRepo() { const currentProjectId = ProjectManager.getCurrentProjectId() || defaultProjectKey; if (!this.projectMessageRepos.has(currentProjectId)) { // Create new repository for this project const repo = new MessageRepository(); this.projectMessageRepos.set(currentProjectId, repo); } return this.projectMessageRepos.get(currentProjectId)!; } ``` ### 3. ChatUIState (`src/state/ChatUIState.ts`) **Purpose**: Clean UI-only state manager **Design Philosophy**: - Delegates ALL business logic to ChatManager - Provides React integration with subscription mechanism - Replaces legacy SharedState with minimal, focused approach **React Integration**: ```typescript // Subscribe to state changes subscribe(listener: () => void): () => void // Delegate operations to ChatManager async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise getMessages(): ChatMessage[] // Computed view for UI // Project and persistence operations async handleProjectSwitch(): Promise // Handle UI updates for project switch async saveChat(modelKey: string): Promise<{ success: boolean; path?: string; error?: string }> // Legacy compatibility (for backward compatibility) get chatHistory(): ChatMessage[] addMessage(message: ChatMessage): void clearChatHistory(): void // Notify React components of changes private notifyListeners(): void ``` ### 4. ContextManager (`src/core/ContextManager.ts`) **Purpose**: Handles context processing and reprocessing with layered context architecture **Key Features**: - **Layered Context System**: Builds structured L1-L5 context layers (see CONTEXT_ENGINEERING.md) - **L2 Auto-Promotion**: Automatically promotes previous turn's context to L2 for cache stability - **Deduplication**: Ensures context appears only once (L3 takes priority over L2) - **Context Processing**: Handles notes, URLs, selected text, tags, and folders - **Reprocessing**: Regenerates fresh context when messages are edited - **Envelope Building**: Creates `PromptContextEnvelope` with structured layers and hashes - **Chain-Aware Processing**: Applies chain-specific rules (e.g., Copilot Plus URL processing, active-note handling for vision models) **Core Methods**: ```typescript // Process context for new message (includes L2 building from history) async processMessageContext( message: ChatMessage, fileParserManager: FileParserManager, vault: Vault, chainType: ChainType, includeActiveNote: boolean, activeNote: TFile | null, messageRepo: MessageRepository, systemPrompt?: string ): Promise // Reprocess context for edited message async reprocessMessageContext(messageId: string, ...): Promise ``` ### 5. ChatPersistenceManager (`src/core/ChatPersistenceManager.ts`) **Purpose**: Handles saving and loading chat history to/from markdown files **Key Features**: - Project-aware file naming (prefixes with project ID) - Filters chat history files based on current project - Parses and formats chat content for storage - Integrated with ChatManager for seamless persistence **Core Methods**: ```typescript // Save chat to markdown file async saveChat(messages: ChatMessage[], modelKey: string, projectId?: string): Promise<{ success: boolean; path?: string; error?: string }> // Get available chat history files async getChatHistoryFiles(): Promise // File naming convention // Project chats: `[projectId]-[timestamp]-[modelKey]-chat.md` // Non-project chats: `[timestamp]-[modelKey]-chat.md` ``` ## Architecture Diagrams ### Complete System Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ User Interface Layer │ ├─────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌──────────────────┐ │ │ │ Chat.tsx │ ◄────── uses ──────────► │ CopilotView.tsx │ │ │ │ │ │ │ │ │ └────────┬────────┘ └──────────────────┘ │ │ │ │ │ │ subscribes to & calls │ │ ▼ │ └───────────┬─────────────────────────────────────────────────────────────────────────┘ │ ┌───────────┴─────────────────────────────────────────────────────────────────────────┐ │ State Layer │ ├─────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ │ ┌────────▼────────┐ │ │ │ ChatUIState │ - React state management │ │ │ │ - Subscription mechanism for UI updates │ │ │ │ - Delegates all business logic to ChatManager │ │ └────────┬────────┘ │ │ │ │ └───────────┴─────────────────────────────────────────────────────────────────────────┘ │ delegates to ┌───────────▼─────────────────────────────────────────────────────────────────────────┐ │ Business Logic Layer │ ├─────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ orchestrates ┌─────────────────────────────┐ │ │ │ ChatManager │ ◄──────────────────────────► │ ContextManager (singleton) │ │ │ │ │ │ │ │ │ │ - Message CRUD │ │ - Process message context │ │ │ │ - Project │ │ - Handle note attachments │ │ │ │ isolation │ │ - Reprocess on edit │ │ │ │ - Memory sync │ └─────────────────────────────┘ │ │ └────────┬────────┘ │ │ │ │ │ │ manages ┌─────────────────────────────┐ │ │ │ │ ChatPersistenceManager │ │ │ ├──────────────────────────────────────►│ │ │ │ │ │ - Save/load chat history │ │ │ │ │ - Project-aware file naming │ │ │ │ └─────────────────────────────┘ │ │ │ │ │ │ coordinates ┌─────────────────────────────┐ │ │ ├──────────────────────────────────────►│ ChainManager │ │ │ │ │ │ │ │ │ │ - Memory management │ │ │ │ │ - LLM chain operations │ │ │ │ └──────────┬──────────────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌─────────────────────────────┐ │ │ │ │ MemoryManager │ │ │ │ │ │ │ │ │ │ - Chain memory storage │ │ │ │ │ - Conversation history │ │ │ │ └─────────────────────────────┘ │ └───────────┴─────────────────────────────────────────────────────────────────────────┘ │ ┌───────────▼─────────────────────────────────────────────────────────────────────────┐ │ Data Storage Layer │ ├─────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ MessageRepository │ │ │ │ │ │ │ │ ┌─────────────────┐ Computed Views ┌────────────────────────────┐ │ │ │ │ │ StoredMessage[] │ ──────────────────────► │ getDisplayMessages() │ │ │ │ │ │ │ │ (for UI rendering) │ │ │ │ │ │ - id │ └────────────────────────────┘ │ │ │ │ │ - displayText │ │ │ │ │ │ - processedText │ ──────────────────────► ┌────────────────────────────┐ │ │ │ │ │ - sender │ │ getLLMMessages() │ │ │ │ │ │ - timestamp │ │ (for AI processing) │ │ │ │ │ │ - context │ └────────────────────────────┘ │ │ │ │ └─────────────────┘ │ │ │ │ │ │ │ │ Single source of truth - no dual storage! │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────┘ ``` ## Project Isolation Architecture ### Multi-Repository Design ``` ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ ChatManager │ ├─────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ projectMessageRepos: Map │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ "defaultProject" │ │ "project-1" │ │ "project-2" │ │ │ │ │ │ │ │ │ │ │ │ MessageRepo │ │ MessageRepo │ │ MessageRepo │ │ │ │ - Non-project │ │ - Project 1 │ │ - Project 2 │ │ │ │ messages │ │ messages only │ │ messages only │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ ▲ ▲ ▲ │ │ │ │ │ │ │ └─────────────────────────┴─────────────────────────┘ │ │ │ │ │ getCurrentMessageRepo() │ │ (auto-detects active project) │ │ │ └──────────────────────────────────────────────────────────────────────────────────────┘ ``` ### Project Switch Flow ``` Project Switch Detected (via Obsidian workspace) ↓ ProjectManager.getCurrentProjectId() returns new ID ↓ ChatManager.getCurrentMessageRepo() ↓ Check if repository exists for project ↓ (if not) Create new MessageRepository ↓ Store in projectMessageRepos Map ↓ Return project-specific repository ``` ## Message Lifecycle ### Example: User Message with Context Note When a user types "Summarize this note" and attaches "meeting-notes.md": 1. **Input**: User text + attached file → Chat component 2. **Storage**: MessageRepository stores displayText: "Summarize this note" 3. **Processing**: ContextManager reads the note and creates processedText with proper XML structure 4. **Memory Sync**: Chain memory receives the processed version for LLM 5. **UI Update**: Shows message with context badge, displays only "Summarize this note" 6. **LLM Processing**: AI receives full context and generates response ### Context XML Format All context is wrapped in semantic XML tags for clear structure: > Layered Prompting: During Phase 3 the raw XML is still captured in `processedText` for backward compatibility, but the canonical representation sent to the LLM comes from the envelope layers: > > - **L1_SYSTEM / L2_PREVIOUS**: Stable prefixes rendered from accumulated context > - **L3_TURN**: Turn-specific smart references that either link back to L2 or embed full content > - **L4_STRIP**: Chat history managed by memory > - **L5_USER**: The user’s raw message (plus composer directives when present) #### Note Context ```xml meeting-notes docs/meeting-notes.md 2024-01-15T10:00:00.000Z 2024-01-15T14:30:00.000Z [actual note content here] ``` #### URL Context ```xml https://example.com/article [fetched content from URL] ``` #### Selected Text Context ```xml Source Note Title path/to/source.md 45 52 [selected text content] ``` #### Error Cases ```xml filename path/to/file.ext [Error: Could not process file] ``` This separation ensures: - Clean UI (shows what user typed) - Rich context for AI (includes note content) - Reprocessable context on message edits For detailed examples, see: - `src/core/MessageLifecycle.test.ts` - Complete lifecycle demonstration with context notes - `src/core/MessageLifecycle.xmltags.test.ts` - XML tag formatting tests and examples ### 1. Sending a New Message ``` User Input ↓ (via Chat component) ChatUIState.sendMessage() ↓ ChatManager.sendMessage() ↓ MessageRepository.addMessage() // Store with basic content ↓ ContextManager.processMessageContext() // Add context ↓ MessageRepository.updateProcessedText() // Update with processed text + context envelope ↓ ChatManager.updateChainMemory() // Sync to LLM ↓ ChatUIState.notifyListeners() // Update UI ``` ### 2. Editing a Message ``` User Edit ↓ ChatUIState.editMessage() ↓ ChatManager.editMessage() ↓ MessageRepository.editMessage() // Update display text ↓ ContextManager.reprocessMessageContext() // Fresh context ↓ ChatManager.updateChainMemory() // Sync to LLM ↓ ChatUIState.notifyListeners() // Update UI ``` ### 3. Message Display ``` React Component Render ↓ ChatUIState.getMessages() ↓ ChatManager.getDisplayMessages() ↓ ChatManager.getCurrentMessageRepo() // Project-aware ↓ MessageRepository.getDisplayMessages() // Computed view ↓ Filter visible messages → Map to ChatMessage format ``` ### 4. Saving Chat History ``` User Save Action ↓ Chat.tsx → ChatUIState.saveChat(modelKey) ↓ ChatManager.saveChat(modelKey) ↓ Get current project ID and messages ↓ ChatPersistenceManager.saveChat(messages, modelKey, projectId) ↓ Create markdown file with project prefix ↓ Return success with file path ``` ### 5. Project Switch ``` Project Change in Obsidian ↓ ChatUIState.handleProjectSwitch() ↓ ChatManager.handleProjectSwitch() ↓ Force getCurrentMessageRepo() to re-detect project ↓ Switch to different MessageRepository ↓ Update chain memory with new project's messages ↓ Notify UI listeners for refresh ``` ## Data Structures ### StoredMessage (Internal) ```typescript interface StoredMessage { id: string; displayText: string; // What user typed/AI responded processedText: string; // With context for user, same as display for AI sender: string; timestamp: FormattedDateTime; context?: MessageContext; isVisible: boolean; isErrorMessage?: boolean; sources?: { title: string; score: number }[]; content?: any[]; } ``` ### ChatMessage (External Interface) ```typescript interface ChatMessage { id?: string; message: string; // Display text originalMessage?: string; // Processed text sender: string; timestamp: FormattedDateTime | null; isVisible: boolean; context?: MessageContext; isErrorMessage?: boolean; sources?: { title: string; score: number }[]; content?: any[]; } ``` ### MessageContext ```typescript interface MessageContext { notes: TFile[]; urls: string[]; selectedTextContexts: SelectedTextContext[]; } ``` ## Chat History Loading ### Pending Message Mechanism The new architecture uses a "pending message" pattern for loading chat history: ``` main.ts.loadChatHistory() ↓ Parse messages from file ↓ CopilotView.setPendingMessages() ↓ Chat component receives pendingMessages prop ↓ useEffect detects pendingMessages ↓ ChatUIState.loadMessages() ↓ onPendingMessagesProcessed() callback clears pending ``` ### Project-Aware Loading When loading chat history: 1. ChatPersistenceManager filters files based on current project 2. Only shows chat files prefixed with current project ID 3. Non-project chats visible when no project is active ## Testing Strategy ### Unit Tests - **MessageRepository**: 23 comprehensive tests including bug prevention - **ChatManager**: 25+ tests covering all critical functionality - **Component Tests**: MessageContext duplicate key prevention ### Bug Prevention Tests 1. **Context Badge Bug**: Ensures context displays correctly 2. **Memory Synchronization**: Prevents chat memory count mismatches 3. **Edit Message Bug**: Verifies proper context reprocessing 4. **Duplicate Notes**: Prevents React key conflicts in context display ## Migration from SharedState ### Before (Legacy) ```typescript // Multiple sources of truth const sharedState = { currentChatMessages: ChatMessage[], chatHistory: ChatMessage[], // Complex sync logic between arrays } ``` ### After (Clean Architecture) ```typescript // Single source of truth const messageRepository = new MessageRepository(); const chatManager = new ChatManager(messageRepository, ...); const chatUIState = new ChatUIState(chatManager); // Computed views const displayMessages = chatUIState.getMessages(); // For UI const llmMessages = chatManager.getLLMMessages(); // For AI ``` ## Performance Considerations ### Memory Efficiency - Single storage eliminates duplicate message objects - Computed views are generated on-demand - Context processing only when needed ### React Optimization - Subscription-based updates minimize re-renders - Unique keys prevent React reconciliation issues - State changes are batched through ChatUIState ## Key Architectural Features ### Project Isolation Benefits 1. **Complete Separation**: Each project has entirely separate chat history 2. **Automatic Management**: No user configuration needed 3. **Seamless Switching**: Instant context switch when changing projects 4. **Memory Efficient**: Only active project's messages in memory 5. **Fresh Start**: Each project starts with empty chat history ### Persistence Integration 1. **Project-Aware Naming**: Files prefixed with project ID 2. **Filtered File Lists**: Only shows relevant chat files 3. **Consistent Format**: Same markdown format across all projects 4. **Error Handling**: Graceful fallbacks for save/load failures ## Troubleshooting ### Common Issues 1. **Context not updating**: Check if `updateChainMemory()` is called after edits 2. **UI not refreshing**: Ensure `notifyListeners()` is called after state changes 3. **Memory count mismatch**: Verify `truncateAfterMessageId()` updates chain memory 4. **Duplicate context badges**: Check React keys in MessageContext component 5. **Wrong project messages**: Check `getCurrentProjectId()` returns expected value 6. **Missing chat history**: Verify project ID in filename matches current project ### Debug Methods ```typescript // Check message repository state messageRepo.getDebugInfo(); // Check chat manager state chatManager.getDebugInfo(); // Check LLM vs display message counts console.log({ display: chatUIState.getMessages().length, llm: chatManager.getLLMMessages().length, }); // Check current project and repository const debugInfo = chatManager.getDebugInfo(); console.log({ currentProject: debugInfo.currentProjectId, totalProjects: debugInfo.projectCount, messagesByProject: debugInfo.messageCountByProject, }); ``` ## Related Files ### Documentation - `docs/CONTEXT_ENGINEERING.md` - Layered context system design and L2 auto-promotion details - `docs/MESSAGE_ARCHITECTURE.md` - This document (message management architecture) ### Core Implementation - `src/core/MessageRepository.ts` - Message storage - `src/core/ChatManager.ts` - Business logic with project isolation - `src/state/ChatUIState.ts` - UI state management - `src/core/ContextManager.ts` - Context processing - `src/core/ChatPersistenceManager.ts` - Chat history persistence ### React Integration - `src/components/Chat.tsx` - Main chat component - `src/hooks/useChatManager.ts` - React hook for ChatUIState - `src/components/chat-components/ChatSingleMessage.tsx` - Message display ### Testing - `src/core/MessageRepository.test.ts` - Repository tests - `src/core/ChatManager.test.ts` - Manager tests - `src/core/MessageLifecycle.test.ts` - Complete lifecycle examples with context notes - `src/core/MessageLifecycle.xmltags.test.ts` - XML tag formatting tests and examples - `src/components/chat-components/MessageContext.test.tsx` - Context display tests