src — persistence
src — persistence
The src/persistence module is responsible for managing the long-term storage, retrieval, and manipulation of conversation data within Code Buddy. It provides mechanisms for persisting chat sessions, handling conversation branches, exporting/replaying sessions, and ensuring data integrity through file locking.
Core Concepts: Sessions, Branches, and Exports
Before diving into the components, it's important to understand the distinct concepts of "sessions," "branches," and "exported sessions" as handled by this module:
- Sessions (Managed by
SessionStore):
- Represent the primary, ongoing conversation history with Code Buddy.
- Persisted as individual JSON files (and partially to SQLite) in the user's home directory (
~/.codebuddy/sessions). - Can be created, loaded, updated, listed, searched, and resumed.
- Support basic metadata, working directory, and model information.
- Designed for continuity across Code Buddy invocations.
- Conversation Branches (Managed by
ConversationBranchManager):
- Allow forking and merging of conversation histories, similar to Git branches.
- Each branch is a distinct sequence of messages, potentially diverging from a parent branch at a specific message index.
- Persisted as individual JSON files in
~/.codebuddy/branches. - Primarily used for exploring alternative conversational paths without losing the original context.
- Exported Sessions (Managed by
SessionRecorder,SessionExporter,SessionPlayer):
- A snapshot of a conversation, designed for sharing, debugging, or replaying.
SessionRecordercaptures messages and metadata in memory.SessionExporterconverts this in-memory snapshot into various formats (JSON, Markdown, HTML).SessionPlayercan load an exported JSON session and replay it, simulating the original conversation flow.- These components operate independently of the live
SessionStoreandConversationBranchManager, focusing on the representation and playback of a session rather than its live management.
Architecture Overview
The persistence module is composed of several distinct classes, each with a specialized role. SessionStore and ConversationBranchManager handle the live, mutable state of conversations, while SessionRecorder, SessionExporter, and SessionPlayer deal with immutable snapshots for export and replay. SessionLock provides a critical utility for SessionStore to prevent data corruption.
graph TD
subgraph Live Conversation Management
SS[SessionStore] -->|Persists & Loads| SessionFiles(Session JSON Files)
SS -->|Uses for concurrency| SL(SessionLock)
SS -->|Partially writes to| DB(SQLite Database)
CBM[ConversationBranchManager] -->|Persists & Loads| BranchFiles(Branch JSON Files)
end
subgraph Session Export & Replay
SR[SessionRecorder] -->|Records messages into| ExportedSession(In-memory Session)
ExportedSession --> SE[SessionExporter]
ExportedSession --> SP[SessionPlayer]
SE -->|Outputs| JSON(JSON String)
SE -->|Outputs| MD(Markdown String)
SE -->|Outputs| HTML(HTML String)
SP -->|Consumes| JSON
end
subgraph Utilities
SPicker[SessionPicker] -->|Reads from| SessionFiles
end
subgraph External Dependencies
SS -- Calls --> GRT[generateConversationTitle (utils)]
SE -- Calls --> DRE[DataRedactionEngine (security)]
SL -- Calls --> Process(OS Process API)
end
style SS fill:#e0f7fa,stroke:#00796b,stroke-width:2px
style CBM fill:#e0f7fa,stroke:#00796b,stroke-width:2px
style SR fill:#fff3e0,stroke:#ff8f00,stroke-width:2px
style SE fill:#fff3e0,stroke:#ff8f00,stroke-width:2px
style SP fill:#fff3e0,stroke:#ff8f00,stroke-width:2px
style SL fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style SPicker fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style SessionFiles fill:#f5f5f5,stroke:#9e9e9e,stroke-dasharray: 5 5
style BranchFiles fill:#f5f5f5,stroke:#9e9e9e,stroke-dasharray: 5 5
style ExportedSession fill:#f5f5f5,stroke:#9e9e9e,stroke-dasharray: 5 5
style DB fill:#f5f5f5,stroke:#9e9e9e,stroke-dasharray: 5 5
Key Components
SessionStore (src/persistence/session-store.ts)
The SessionStore is the primary component for managing the persistence of Code Buddy's main conversation sessions. It handles saving and loading session data, including messages and metadata, to disk.
Key Features:
- Session Management:
createSession,loadSession,updateCurrentSession,addMessageToCurrentSession,deleteSession,resumeSession. - Storage: Sessions are primarily stored as JSON files in
~/.codebuddy/sessions. - It also has a
useSQLiteconfiguration option. When enabled,createSessionandaddMessageToCurrentSessionwill write basic session and message data to an SQLite database viaSessionRepository. However,loadSessionandlistSessionsstill read exclusively from JSON files. This means SQLite is used for recording new data, but not for retrieving full session state or listing existing sessions. - Auto-Save: Supports an
autoSavemode to automatically persist changes. - Ephemeral Mode: Sessions can be marked as
ephemeral(setEphemeral(true)), preventing them from being written to disk. Useful for one-off queries. - Session Locking: Uses
SessionLockto prevent concurrent writes to the same session file, ensuring data integrity. - Task State Persistence: Provides
saveTaskStateandloadTaskStatefor cross-session continuity of agent task states. - Export: Includes methods to
exportToMarkdown,exportToJson, andexportToHtmlfor a given session. - Session Discovery:
listSessions,getRecentSessions,searchSessions,getLastSession,getSessionByPartialId. - Branching/Cloning:
cloneSessionandbranchSessionallow creating new sessions based on existing ones. - Singleton: Accessed via
getSessionStore().
Usage Example:
import { getSessionStore } from 39;./persistence/session-store.js39;;
import type { ChatEntry } from 39;../agent/types.js39;;
const sessionStore = getSessionStore();
async function manageSession() {
class="hl-cmt">// Create a new session
const newSession = await sessionStore.createSession("My New Project Chat");
console.log(`Created session: ${newSession.id}`);
class="hl-cmt">// Add a message
const userMessage: ChatEntry = {
type: 39;user39;,
content: 39;Hello Code Buddy, how can I refactor this?39;,
timestamp: new Date(),
};
await sessionStore.addMessageToCurrentSession(userMessage);
class="hl-cmt">// Load a session
const loadedSession = await sessionStore.loadSession(newSession.id);
if (loadedSession) {
console.log(`Loaded session "${loadedSession.name}" with ${loadedSession.messages.length} messages.`);
}
class="hl-cmt">// List recent sessions
const recent = await sessionStore.getRecentSessions(5);
console.log("Recent sessions:\n", sessionStore.formatSessionList());
class="hl-cmt">// Export to Markdown
const markdownPath = await sessionStore.exportSessionToFile(newSession.id, 39;my-session.md39;);
console.log(`Session exported to ${markdownPath}`);
}
manageSession();
ConversationBranchManager (src/persistence/conversation-branches.ts)
The ConversationBranchManager enables Git-like branching and merging of conversation histories. This is crucial for exploring different solutions or conversational paths without losing the original context.
Key Features:
- Branch Lifecycle:
createBranch,fork,forkFromMessage,checkout,deleteBranch,renameBranch. - Message Management:
addMessage,setMessages,getMessages(for the current branch). - Merging:
mergeallows combining messages from one branch into another, with "append" or "replace" strategies. IncludesfindCommonAncestorfor intelligent merging. - Persistence: Branches are stored as individual JSON files in
~/.codebuddy/branches. - Branch Traversal:
getAllBranches,getBranchTree,getBranchHistory. - Event Emitter: Emits events like
branch:created,branch:forked,branch:checkout,branch:merged,branch:deleted,branch:renamed. - Formatting:
formatBranchesandformatBranchTreeprovide human-readable output for CLI. - Singleton: Accessed via
getBranchManager().
Usage Example:
import { getBranchManager } from 39;./persistence/conversation-branches.js39;;
import { CodeBuddyMessage } from 39;../codebuddy/client.js39;;
const branchManager = getBranchManager();
async function manageBranches() {
class="hl-cmt">// Ensure 39;main39; branch exists and is current
branchManager.checkout("main");
class="hl-cmt">// Add some initial messages
branchManager.addMessage({ role: 39;user39;, content: 39;Initial query39; } as CodeBuddyMessage);
branchManager.addMessage({ role: 39;assistant39;, content: 39;Initial response39; } as CodeBuddyMessage);
class="hl-cmt">// Fork a new branch
const featureBranch = branchManager.fork("feature-a");
console.log(`Forked to branch: ${featureBranch.name} (${featureBranch.id})`);
class="hl-cmt">// Add messages to the feature branch
branchManager.addMessage({ role: 39;user39;, content: 39;On feature-a: new question39; } as CodeBuddyMessage);
branchManager.addMessage({ role: 39;assistant39;, content: 39;On feature-a: new answer39; } as CodeBuddyMessage);
class="hl-cmt">// Checkout back to main
branchManager.checkout("main");
branchManager.addMessage({ role: 39;user39;, content: 39;On main: another question39; } as CodeBuddyMessage);
class="hl-cmt">// Merge feature-a into main
branchManager.merge(featureBranch.id, "append");
console.log(`Merged ${featureBranch.name} into main.`);
console.log(branchManager.formatBranches());
console.log(branchManager.formatBranchTree());
}
manageBranches();
SessionRecorder, SessionExporter, SessionPlayer (src/persistence/session-export.ts)
These three classes work together to provide advanced capabilities for recording, exporting, and replaying conversation sessions. They operate on an ExportedSession data structure, which is a self-contained snapshot of a conversation.
SessionRecorder
Records messages, tool calls, and metadata into an in-memory ExportedSession object.
Key Features:
- Recording:
start(),stop(),addMessage,addUserMessage,addAssistantMessage,addToolResult. - Metadata:
updateUsage,createCheckpoint,addTags,setSummary. - Event Emitter: Emits
recording:started,recording:stopped,message:added,checkpoint:created. - Singleton:
getSessionRecorder()provides a global instance for live recording.
SessionExporter
Takes an ExportedSession and converts it into various output formats.
Key Features:
- Export Formats:
export()method supportsjson,markdown, andhtml. - File Export:
exportToFile()simplifies writing to a file, inferring format from extension. - Options: Supports options for redacting secrets, including tool results, metadata, and checkpoints.
- Data Redaction: Integrates with
getDataRedactionEnginefromsrc/security/data-redaction.tsto redact sensitive information.
SessionPlayer
Loads an ExportedSession (typically from a JSON file) and replays it, simulating the original conversation flow.
Key Features:
- Loading:
loadFromFile(),load(). - Replay Control:
replay(),pause(),resume(),stop(). - Navigation:
jumpToCheckpoint(),jumpToIndex(). - Replay Options: Configurable speed, start/stop points, pausing at tool calls, skipping tool execution.
- Event Emitter: Emits
loaded,replay:started,replay:ended,message,toolcall,paused,resumed,stopped,paused:toolcall,jumped.
Usage Example (Recorder/Exporter):
import { getSessionRecorder, SessionExporter } from 39;./persistence/session-export.js39;;
const recorder = getSessionRecorder(); class="hl-cmt">// Global singleton
recorder.start();
recorder.addUserMessage("What39;s the capital of France?");
recorder.addAssistantMessage("Paris.");
recorder.updateUsage(10, 0.0001);
recorder.createCheckpoint("Initial Q&A");
const sessionData = recorder.getSession();
recorder.stop();
const exporter = new SessionExporter();
const markdownOutput = exporter.export(sessionData, { format: 39;markdown39; });
console.log(markdownOutput);
class="hl-cmt">// To export to file:
class="hl-cmt">// await exporter.exportToFile(sessionData, 39;exported-session.html39;, { format: 39;html39; });
Usage Example (Player):
import { SessionPlayer } from 39;./persistence/session-export.js39;;
import * as fs from 39;fs/promises39;;
async function replayExample() {
class="hl-cmt">// Assume 39;exported-session.json39; exists from a previous export
const player = new SessionPlayer();
await player.loadFromFile(39;exported-session.json39;);
player.on(39;message39;, ({ message, index }) => {
console.log(`[${index}] ${message.role}: ${message.content.slice(0, 50)}...`);
});
player.on(39;replay:ended39;, () => console.log(39;Replay finished.39;));
await player.replay({ speed: 2, pauseAtToolCalls: true }); class="hl-cmt">// 2x speed, pause on tools
player.dispose();
}
class="hl-cmt">// replayExample();
SessionLock (src/persistence/session-lock.ts)
Provides a file-based locking mechanism to prevent multiple processes from concurrently writing to the same session file, which could lead to data corruption.
Key Features:
- PID-based Locking: Creates a
.lockfile alongside the target session file, containing the PID of the process holding the lock. - Stale Lock Detection: Automatically cleans up locks held by dead processes or locks that are older than
LOCK_STALE_MS(1 minute). - Atomic Acquisition: Uses
fs.writeFileSyncwith thewxflag for atomic file creation, preventing race conditions. - Process Exit Cleanup: Registers handlers to release the lock when the process exits or receives termination signals.
withSessionLockUtility: A convenient async function to wrap operations that require a lock, ensuring it's acquired and released correctly.
Usage Example (Internal to SessionStore):
class="hl-cmt">// Inside SessionStore.saveSession:
import { withSessionLock } from 39;./session-lock.js39;;
class="hl-cmt">// ...
const filePath = this.getSessionFilePath(session.id);
await withSessionLock(filePath, async () => {
await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2));
});
class="hl-cmt">// ...
SessionPicker (src/persistence/session-picker.ts)
A utility class for browsing and formatting session entries, primarily for CLI or UI display.
Key Features:
- Entry Management: Stores and provides access to
SessionPickerEntryobjects. - Sorting:
getEntries()returns entries sorted bylastAccessed. - Searching:
searchByBranch(),searchByName(). - Formatting:
formatEntry()andformatTable()provide structured string output for display.
Usage Example:
import { SessionPicker, SessionPickerEntry } from 39;./persistence/session-picker.js39;;
const entries: SessionPickerEntry[] = [
{ id: 39;session_abc12339;, name: 39;Refactor Utility39;, branch: 39;main39;, messageCount: 25, lastAccessed: Date.now() - 100000, tags: [39;refactor39;] },
{ id: 39;session_def45639;, name: 39;New Feature Idea39;, branch: 39;feature-x39;, messageCount: 10, lastAccessed: Date.now() - 50000, tags: [39;feature39;] },
];
const picker = new SessionPicker(entries);
console.log(picker.formatTable(picker.getEntries()));
class="hl-cmt">// Output:
class="hl-cmt">// ID Name Branch Messages Last Used
class="hl-cmt">// --------------------------------------------------
class="hl-cmt">// session_d New Feature Idea feature-x 10 2023-10-27
class="hl-cmt">// session_a Refactor Utility main 25 2023-10-27
Data Models
The persistence module defines several key interfaces to structure conversation data:
ConversationBranch: Represents a single branch in the conversation tree.
interface ConversationBranch {
id: string;
name: string;
parentId?: string;
parentMessageIndex?: number;
messages: CodeBuddyMessage[]; class="hl-cmt">// From ../codebuddy/client.js
createdAt: Date;
updatedAt: Date;
metadata?: BranchMetadata;
}
BranchMetadata: Additional information for aConversationBranch.Session: Represents a live, ongoing conversation session.
interface Session {
id: string;
name: string;
workingDirectory: string;
model: string;
messages: SessionMessage[];
createdAt: Date;
lastAccessedAt: Date;
metadata?: SessionMetadata;
}
SessionMessage: A message within aSession, a more detailed type thanCodeBuddyMessagefor persistence.
interface SessionMessage {
type: 39;user39; | 39;assistant39; | 39;tool_result39; | 39;tool_call39; | 39;reasoning39; | 39;plan_progress39; | 39;steer39; | 39;diff_preview39;;
content: string;
timestamp: string;
toolCallName?: string;
toolCallSuccess?: boolean;
taskState?: Record<string, unknown>; class="hl-cmt">// For cross-session continuity
}
SessionMetadata: Additional information for aSession.ExportedSession: A snapshot of a session, used for export and replay.
interface ExportedSession {
version: string;
exportedAt: number;
metadata: SessionMetadata;
messages: SessionMessage[];
checkpoints?: SessionCheckpoint[];
}
SessionCheckpoint: A marker within anExportedSessionfor replay.SessionPickerEntry: A simplified view of a session for listing/picking.
Integration Points & Dependencies
The persistence module interacts with several other parts of the Code Buddy codebase:
src/codebuddy/client.ts:CodeBuddyMessagetype is used withinConversationBranch.src/utils/logger.ts: Used for logging warnings and debug information.src/utils/conversation-title.ts:generateConversationTitleis used bySessionStoreto auto-name sessions.src/security/data-redaction.ts:getDataRedactionEngineis used bySessionExporterto redact sensitive data.src/database/repositories/session-repository.ts:SessionStoreinteracts with this repository when SQLite is enabled.src/agent/types.ts:ChatEntrytype is converted toSessionMessagebySessionStore.commands/handlers/*: Command handlers (e.g.,branch-handlers.ts) directly interact withConversationBranchManagerandSessionStoreto implement CLI commands.ui/components/*: UI components (e.g.,SessionTimeline.tsx) might queryConversationBranchManagerfor branch information.- Node.js built-in modules:
fs,fs/promises,path,os,events. - Third-party modules:
fs-extra(forConversationBranchManager).
Usage Patterns & Entry Points
Developers should primarily interact with this module through its singleton instances and factory functions:
getSessionStore(): The main entry point for managing live chat sessions.getBranchManager(): The main entry point for managing conversation branches.getSessionRecorder(): The global instance for recording messages for export.new SessionExporter(): Instantiate directly to export session data.new SessionPlayer(): Instantiate directly to replay session data.exportSession()/replaySession(): Convenience functions for quick export/replay operations.
Important Note on Exports:
The src/persistence/index.ts file only re-exports conversation-branches.ts. This means that SessionStore, SessionRecorder, SessionExporter, SessionPlayer, SessionLock, and SessionPicker must be imported directly from their respective files (e.g., import { getSessionStore } from './persistence/session-store.js';).
Contribution Guidelines
- Consistency: When adding new persistence mechanisms or modifying existing ones, ensure consistency in how data is stored (e.g., JSON format, date serialization).
- File Locking: For any new file-based persistence, consider if
SessionLockor a similar mechanism is needed to prevent concurrent write issues. - SQLite Integration: If extending
SessionStore's SQLite integration, ensure that both read and write paths are fully implemented and tested, and that the JSON file fallback remains robust. - Error Handling: Persistence operations can fail due to file system issues. Implement robust
try-catchblocks and appropriate logging (usinglogger) to handle these gracefully. - Performance: For operations involving many sessions or large message histories, consider performance implications, especially for synchronous file system operations.
- Testability: Design components to be easily testable, ideally by allowing dependency injection or providing clear interfaces. The
resetBranchManager()andresetSessionRecorder()functions are provided for testing purposes.