* Implement context engine for a layered context * Wire in new context library to LLM chain - Updated the context engine to maintain a cumulative library of all context items (L2) without deduplication, enhancing cache stability. - Introduced smart referencing in the user message (L3) to reference existing items in L2 by ID and include full content for new items. - Improved the LayerToMessagesConverter to format and handle layered context envelopes for better debugging and clarity. - Fixed critical bugs related to context deduplication that previously broke cache stability. - Enhanced tests to validate the new context handling and ensure proper functionality across various scenarios. * Enhance VaultQAChainRunner with tag extraction and inline citation detection - Integrated tag extraction from user queries to improve context-aware retrieval. - Implemented `hasInlineCitations` function to detect inline citation markers in responses. - Updated message construction logic to utilize envelope-based context, enhancing the quality of AI interactions. - Added comprehensive tests for the new inline citation detection functionality, ensuring robustness and accuracy. - Improved logging for better debugging and traceability of the message processing flow. Refactor VaultQAChainRunner to enhance context handling and citation instructions - Integrated LayerToMessagesConverter for improved message construction, ensuring smart referencing and context preservation. - Updated logging to reflect the use of the new converter in envelope-based context construction. - Revised comments for clarity on context preparation and citation instructions. - Minor adjustment in citation utility to improve regex handling for citation updates. * Complete migration to envelope-based context in CopilotPlus and LLM chain runners - Removed all legacy fallback paths, ensuring that context envelopes are now mandatory for operations in CopilotPlusChainRunner and LLMChainRunner. - Enhanced image extraction to only pull from the active note, preventing unintended leaks from attached context notes. - Fixed critical issues with context-in-context (CiC) formatting and added early envelope guards in VaultQAChainRunner to prevent silent failures. - Updated message construction logic to utilize LayerToMessagesConverter for improved smart referencing and context preservation. - Comprehensive tests added to validate the new envelope-based context handling and ensure robustness across all chain runners. * Integrate Plus chain with context envelope * Enhance AutonomousAgentChainRunner with envelope-based context integration - Introduced `LayerToMessagesConverter` for improved message construction, ensuring system and user messages are derived from the context envelope. - Implemented envelope validation to ensure context is available before processing. - Updated message preparation logic to maintain consistency with the envelope-first architecture, preserving tool execution and multimodal support. - Added comprehensive logging for envelope-based context construction to aid debugging. - Documented changes in the context engineering documentation to reflect the new integration and design points. * Support loading saved chat context to L2 context library * Fix ChatPersistenceManager tests for context handling
33 KiB
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.
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
StoredMessageobjects with bothdisplayTextandprocessedText 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:
// 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
ChatManagercallsaddMessagewith a fullNewChatMessage, it includes thecontextEnvelopeproperty so the repository keeps both the legacyprocessedTextand 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:
- User sends a message →
ContextManager.processMessageContext()returns bothprocessedContentand aPromptContextEnvelope. ChatManagerstores the envelope withMessageRepository.updateProcessedText(...).- When any chain runner executes, it reads
userMessage.contextEnvelopeand feeds it toLayerToMessagesConverter.convert()to materialize the L1-L5 prompt structure. - Regeneration or edits call
reprocessMessageContext, ensuring a fresh envelope replaces the stale one.
Key Operations:
// Send new message with context processing
async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise<string>
// Edit message and reprocess context
async editMessage(messageId: string, newText: string, chainType: ChainType, includeActiveNote?: boolean): Promise<boolean>
// Regenerate AI response
async regenerateMessage(messageId: string, onUpdateMessage: Function, onAddMessage: Function): Promise<boolean>
// Memory synchronization
private async updateChainMemory(): Promise<void>
// Project management
private getCurrentMessageRepo(): MessageRepository // Auto-detects current project
async handleProjectSwitch(): Promise<void> // Forces project detection
// Persistence
async saveChat(modelKey: string): Promise<{ success: boolean; path?: string; error?: string }>
Project Isolation Implementation:
// Internal structure
private projectMessageRepos: Map<string, MessageRepository>
// 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:
// Subscribe to state changes
subscribe(listener: () => void): () => void
// Delegate operations to ChatManager
async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise<string>
getMessages(): ChatMessage[] // Computed view for UI
// Project and persistence operations
async handleProjectSwitch(): Promise<void> // 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
PromptContextEnvelopewith 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:
// 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<ContextProcessingResult>
// Reprocess context for edited message
async reprocessMessageContext(messageId: string, ...): Promise<void>
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:
// 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<TFile[]>
// 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<string, MessageRepository> │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ "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":
- Input: User text + attached file → Chat component
- Storage: MessageRepository stores displayText: "Summarize this note"
- Processing: ContextManager reads the note and creates processedText with proper XML structure
- Memory Sync: Chain memory receives the processed version for LLM
- UI Update: Shows message with context badge, displays only "Summarize this note"
- 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
processedTextfor 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
<note_context>
<title>meeting-notes</title>
<path>docs/meeting-notes.md</path>
<ctime>2024-01-15T10:00:00.000Z</ctime>
<mtime>2024-01-15T14:30:00.000Z</mtime>
<content>
[actual note content here]
</content>
</note_context>
URL Context
<url_content>
<url>https://example.com/article</url>
<content>
[fetched content from URL]
</content>
</url_content>
Selected Text Context
<selected_text>
<title>Source Note Title</title>
<path>path/to/source.md</path>
<start_line>45</start_line>
<end_line>52</end_line>
<content>
[selected text content]
</content>
</selected_text>
Error Cases
<note_context_error>
<title>filename</title>
<path>path/to/file.ext</path>
<error>[Error: Could not process file]</error>
</note_context_error>
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 notessrc/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)
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)
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
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:
- ChatPersistenceManager filters files based on current project
- Only shows chat files prefixed with current project ID
- 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
- Context Badge Bug: Ensures context displays correctly
- Memory Synchronization: Prevents chat memory count mismatches
- Edit Message Bug: Verifies proper context reprocessing
- Duplicate Notes: Prevents React key conflicts in context display
Migration from SharedState
Before (Legacy)
// Multiple sources of truth
const sharedState = {
currentChatMessages: ChatMessage[],
chatHistory: ChatMessage[],
// Complex sync logic between arrays
}
After (Clean Architecture)
// 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
- Complete Separation: Each project has entirely separate chat history
- Automatic Management: No user configuration needed
- Seamless Switching: Instant context switch when changing projects
- Memory Efficient: Only active project's messages in memory
- Fresh Start: Each project starts with empty chat history
Persistence Integration
- Project-Aware Naming: Files prefixed with project ID
- Filtered File Lists: Only shows relevant chat files
- Consistent Format: Same markdown format across all projects
- Error Handling: Graceful fallbacks for save/load failures
Troubleshooting
Common Issues
- Context not updating: Check if
updateChainMemory()is called after edits - UI not refreshing: Ensure
notifyListeners()is called after state changes - Memory count mismatch: Verify
truncateAfterMessageId()updates chain memory - Duplicate context badges: Check React keys in MessageContext component
- Wrong project messages: Check
getCurrentProjectId()returns expected value - Missing chat history: Verify project ID in filename matches current project
Debug Methods
// 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 detailsdocs/MESSAGE_ARCHITECTURE.md- This document (message management architecture)
Core Implementation
src/core/MessageRepository.ts- Message storagesrc/core/ChatManager.ts- Business logic with project isolationsrc/state/ChatUIState.ts- UI state managementsrc/core/ContextManager.ts- Context processingsrc/core/ChatPersistenceManager.ts- Chat history persistence
React Integration
src/components/Chat.tsx- Main chat componentsrc/hooks/useChatManager.ts- React hook for ChatUIStatesrc/components/chat-components/ChatSingleMessage.tsx- Message display
Testing
src/core/MessageRepository.test.ts- Repository testssrc/core/ChatManager.test.ts- Manager testssrc/core/MessageLifecycle.test.ts- Complete lifecycle examples with context notessrc/core/MessageLifecycle.xmltags.test.ts- XML tag formatting tests and examplessrc/components/chat-components/MessageContext.test.tsx- Context display tests