src — checkpoints
src — checkpoints
The src/checkpoints module provides a suite of tools for managing and restoring file states within a project. It offers various strategies, from simple in-memory undo/redo to persistent, Git-backed versioning, catering to different needs for state preservation and recovery.
Core Concepts
At the heart of this module are two fundamental interfaces:
FileSnapshot: Represents the state of a single file at a given moment. It includes the file'spath, itscontent, and a booleanexistedindicating if the file was present when the snapshot was taken.Checkpoint: A collection ofFileSnapshots, representing the state of multiple files (or the entire workspace) at a specific point in time. Each checkpoint has a uniqueid,timestamp,description, and theworkingDirectoryit was created in.
Checkpointing Strategies
The module implements four distinct checkpointing mechanisms, each with its own purpose and persistence model.
1. In-Memory Checkpoints (CheckpointManager)
The CheckpointManager provides a lightweight, session-scoped undo/redo capability. It stores checkpoints in an in-memory array, making it suitable for temporary state management during an active session.
Purpose:
- Quick, temporary undo/redo within a single application session.
- Capturing file states before minor modifications or operations.
How it Works:
- Checkpoints are stored in a private
checkpoints: Checkpoint[]array. - When
createCheckpointis called, it takes snapshots of specified files usingfs.readFileSyncand adds them to the array. - A
maxCheckpointslimit (default 50) ensures the array doesn't grow indefinitely, with older checkpoints beingshift()ed out. rewindTorestores files from a specified checkpoint by writing their content back to disk or deleting them if they didn't exist in the snapshot.- It emits
checkpoint-createdandrewindevents.
Key API:
constructor(options?: CheckpointManagerOptions): Initializes the manager with optionalmaxCheckpointsandautoCheckpointsettings.createCheckpoint(description: string, files?: string[]): Checkpoint: Creates a new checkpoint, optionally for a specific set of files.checkpointBeforeEdit(filePath: string, description?: string): Checkpoint: Convenience method to snapshot a file before it's edited.checkpointBeforeCreate(filePath: string, description?: string): Checkpoint: Convenience method to snapshot a file before it's created (useful for tracking non-existent files).rewindTo(checkpointId: string): Restores the workspace to the state of a given checkpoint.rewindToLast(): Restores to the most recent checkpoint.getCheckpoints(): Returns all stored checkpoints.formatCheckpointList(): Provides a human-readable list of checkpoints.
Usage Example:
import { getCheckpointManager } from 39;./checkpoint-manager.js39;;
const manager = getCheckpointManager();
class="hl-cmt">// Before making a change
manager.checkpointBeforeEdit(39;src/my-file.ts39;, 39;Before refactoring function X39;);
class="hl-cmt">// ... perform file modifications ...
class="hl-cmt">// If something goes wrong, rewind
const result = manager.rewindToLast();
if (result.success) {
console.log(39;Successfully rewound:39;, result.restored);
} else {
console.error(39;Failed to rewind:39;, result.errors);
}
console.log(manager.formatCheckpointList());
Integration:
The CheckpointManager is used by src/agent/tool-executor.ts to create checkpoints before tool dispatches, allowing for immediate undo of agent actions.
2. Persistent Checkpoints (PersistentCheckpointManager)
The PersistentCheckpointManager extends the concept of checkpoints by storing them on disk, providing cross-session persistence. This is inspired by tools like Gemini CLI's /restore command.
Purpose:
- Maintain a history of file states across application restarts.
- Provide a robust undo/restore mechanism for a specific project.
How it Works:
- Checkpoints are stored in a dedicated directory structure:
~/.codebuddy/history/./ - A
project_hashis generated from theworkingDirectoryto isolate checkpoints per project. - Each checkpoint is saved as a separate JSON file (
) within the project's history directory..json - An
index.jsonfile tracks the order and IDs of checkpoints for the project. loadIndexandsaveIndexmanage this index file.loadCheckpointandsaveCheckpointhandle reading/writing individual checkpoint files, with acheckpointCachefor performance.restorefunctions similarly torewindTobut operates on disk-loaded checkpoints.
Key API:
constructor(options?: PersistentCheckpointManagerOptions): Initializes the manager, setting up the history directory.createCheckpoint(description: string, files?: string[]): PersistentCheckpoint: Creates and saves a new persistent checkpoint.restore(checkpointId: string): Restores the workspace to a specific persistent checkpoint.restoreLast(): Restores to the most recent persistent checkpoint.getCheckpoints(): Retrieves all persistent checkpoints for the current project.clearCheckpoints(): Deletes all checkpoints for the current project from disk.getStats(): Provides statistics like count, total files, and storage size.
Usage Example:
import { getPersistentCheckpointManager } from 39;./persistent-checkpoint-manager.js39;;
const manager = getPersistentCheckpointManager();
class="hl-cmt">// On application start, load existing checkpoints
console.log(manager.formatCheckpointList());
class="hl-cmt">// Create a checkpoint before a major operation
manager.createCheckpoint(39;Before attempting complex refactor39;);
class="hl-cmt">// ... application runs, potentially restarts ...
class="hl-cmt">// Restore to a specific checkpoint from the list
const result = manager.restore(39;cp_123abc_def45639;);
if (result.success) {
console.log(39;Restored to checkpoint:39;, result.checkpoint?.description);
}
Integration:
The PersistentCheckpointManager is used by commands/handlers/extra-handlers.ts for the /undo command, allowing users to revert to previous states. src/features/index.ts also uses it to report feature status.
3. Git-based Ghost Snapshots (GhostSnapshotManager)
The GhostSnapshotManager leverages Git to create "ghost" commits that capture the workspace state without polluting the user's main Git history. This is ideal for automatic, frequent snapshots, especially for agent-driven development, enabling easy undo/redo of agent turns.
Purpose:
- Automatic, lightweight snapshots of the workspace before each agent turn.
- Provides a robust undo/redo mechanism for agent actions.
- Keeps snapshot history separate from the main Git history.
How it Works:
- It uses Node.js
child_process.execFileto run Git commands. initialize()checks if the current working directory is a Git repository.createSnapshot():
- Stages all changes (
git add -A). - If there are changes, it creates a commit (
git commit --allow-empty -m "[ghost]...") but then immediately performs agit reset --soft HEAD~1to unstage the changes, leaving them in the working directory. - The commit hash is then stored as a Git reference under a special namespace:
refs/codebuddy/ghost/. This makes the commit reachable by its ref but not part of any branch. - If no changes, it just records the current
HEADhash.
restoreSnapshot(): Usesgit checkoutto restore the working directory to the state of a ghost commit.-- . undoLastTurn()andredoLastTurn()manage a stack of snapshots to facilitate navigation through the history.- Old snapshots are pruned to
MAX_GHOST_SNAPSHOTS(default 50).
Key API:
constructor(cwd?: string): Initializes the manager for a given working directory.initialize(): Checks if the current directory is a Git repo.createSnapshot(description?: string): Creates a ghost snapshot, returningnullif not in a Git repo or if an error occurs.restoreSnapshot(snapshotId: string): Restores the workspace to a specific ghost snapshot.undoLastTurn(): Reverts to the previous ghost snapshot.redoLastTurn(): Reapplies a previously undone snapshot.getTimeline(): Returns all snapshots and the current navigation state.listSnapshots(): Returns all ghost snapshots.
Usage Example:
import { getGhostSnapshotManager } from 39;./ghost-snapshot.js39;;
const manager = getGhostSnapshotManager();
await manager.initialize();
if (manager.isGitRepo) {
class="hl-cmt">// Before an agent performs an action
await manager.createSnapshot(39;Agent turn 1: Implemented feature X39;);
class="hl-cmt">// ... agent modifies files ...
class="hl-cmt">// If the user wants to undo the last agent turn
const undone = await manager.undoLastTurn();
if (undone) {
console.log(39;Undone to:39;, undone.description);
}
class="hl-cmt">// If the user wants to redo
const redone = await manager.redoLastTurn();
if (redone) {
console.log(39;Redone to:39;, redone.description);
}
} else {
console.log(39;Not in a Git repository, ghost snapshots are disabled.39;);
}
Integration:
The GhostSnapshotManager is primarily used for features inspired by OpenAI Codex CLI, as indicated by codex-inspired-features.test.ts.
4. Checkpoint Versioning (CheckpointVersioning)
The CheckpointVersioning system provides a more advanced, Git-like version control layer on top of the basic Checkpoint concept. It allows for named versions, branches, tags, and diffing capabilities.
Purpose:
- Advanced version control for checkpoints, including branching and tagging.
- Detailed history tracking with metadata.
- Ability to compare states between versions.
How it Works:
- It maintains an in-memory graph of
Versionobjects, where eachVersionwraps aCheckpointand includes additional metadata (parent ID, branch name, author, tags). createVersion()generates a content-based hash for the version ID and links it to a parent version, forming a history chain.createBranch()andswitchBranch()manage different lines of development.createTag()allows assigning human-readable names to specific versions.diff()compares the file contents between two versions, providingadded,modified,deletedlists and detailedDiffHunks for modified files.checkout()restores the workspace to a specific version, similar to Git checkout, with rollback capabilities on failure.- State (versions, branches, tags) is persisted to disk in
.codebuddy/versions/versions.jsonusingfs-extrafor cross-session availability.save()andload()handle this. prune()cleans up old versions based on amaxVersionsPerBranchlimit.
Key API:
constructor(config?: VersioningConfig): Initializes the manager with configuration for storage, max versions, and default branch.createVersion(checkpoint: Checkpoint, options?: { name?: string; description?: string; metadata?: Partial: Creates a new version from an existing}): Version Checkpoint.createTag(versionId: string, tagName: string): Tags a specific version.createBranch(name: string, fromVersionId?: string, description?: string): Branch: Creates a new branch, optionally from an existing version.switchBranch(name: string): Branch: Changes the active branch.checkout(versionId: string): Restores the workspace to the state of a specific version.diff(fromVersionId: string, toVersionId: string): VersionDiff: Computes the difference between two versions.findCommonAncestor(versionId1: string, versionId2: string): Version | undefined: Finds the nearest common ancestor of two versions.save(): Persists the versioning state to disk.load(): Loads the versioning state from disk.getVersionHistory(): Retrieves the history of versions for the current or specified branch.
Usage Example:
import { getCheckpointVersioning } from 39;./checkpoint-versioning.js39;;
import { CheckpointManager, getCheckpointManager } from 39;./checkpoint-manager.js39;;
const versioning = getCheckpointVersioning();
const checkpointManager = getCheckpointManager(); class="hl-cmt">// CheckpointVersioning uses Checkpoint objects
await versioning.load(); class="hl-cmt">// Load previous state
class="hl-cmt">// Create a base checkpoint
const initialCp = checkpointManager.createCheckpoint(39;Initial project setup39;);
const v1 = versioning.createVersion(initialCp, { name: 39;v1.039;, description: 39;First stable version39; });
versioning.createTag(v1.id, 39;release-1.039;);
class="hl-cmt">// Create a new branch for a feature
versioning.createBranch(39;feature/new-ui39;, v1.id);
versioning.switchBranch(39;feature/new-ui39;);
class="hl-cmt">// ... make changes, create more checkpoints ...
const featureCp = checkpointManager.createCheckpoint(39;Added new UI component39;);
const v2 = versioning.createVersion(featureCp, { description: 39;New UI component added39; });
class="hl-cmt">// Diff between versions
const diffResult = versioning.diff(v1.id, v2.id);
console.log(39;Diff:39;, diffResult);
class="hl-cmt">// Checkout a previous version
await versioning.checkout(v1.id);
await versioning.save(); class="hl-cmt">// Save current state
Integration:
The CheckpointVersioning module is tested by checkpoint-versioning.test.ts, indicating its role in providing advanced version control features.
Module Architecture Overview
The src/checkpoints module is designed with distinct responsibilities for each manager. While CheckpointVersioning wraps Checkpoint objects, the managers generally operate independently, each providing a specialized form of state management.
graph TD
subgraph Checkpoints Module
A[CheckpointManager] -->|Manages in-memory| B(Checkpoint[])
A -->|Uses| C(fs, path)
A -->|Emits events| D(EventEmitter)
E[PersistentCheckpointManager] -->|Manages on disk| F(Checkpoint files in ~/.codebuddy/history)
E -->|Uses| C
E -->|Uses| G(crypto for project hashing)
E -->|Emits events| D
H[GhostSnapshotManager] -->|Manages via Git| I(Git refs/codebuddy/ghost)
H -->|Uses| J(child_process.execFile for 'git' commands)
H -->|Emits events| D
K[CheckpointVersioning] -->|Manages version graph| L(Version Map)
K -->|Wraps| B
K -->|Uses| M(fs-extra, crypto)
K -->|Persists to| N(.codebuddy/versions/versions.json)
K -->|Emits events| D
end
style B fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#f9f,stroke:#333,stroke-width:2px
style I fill:#f9f,stroke:#333,stroke-width:2px
style L fill:#f9f,stroke:#333,stroke-width:2px
Singleton Access
Each checkpoint manager provides a singleton instance via a get...Manager() function (e.g., getCheckpointManager(), getPersistentCheckpointManager()). This ensures that only one instance of each manager exists throughout the application's lifecycle, maintaining a consistent state. A corresponding reset...Manager() function is available for testing or explicit cleanup.
When to Use Which Manager
CheckpointManager: For simple, temporary undo/redo within a single session. Ideal for capturing states before minor, reversible operations.PersistentCheckpointManager: For robust, project-specific undo/restore that needs to survive application restarts. Useful for user-initiated "save points" or major operational rollbacks.GhostSnapshotManager: For automatic, frequent, and non-intrusive snapshots, especially in agent-driven workflows where an undo/redo of "turns" is desired without affecting the main Git history.CheckpointVersioning: For advanced version control needs, including branching, tagging, and detailed diffing, where a more structured history and comparison capabilities are required. This is suitable for more complex development scenarios or internal tooling that needs a full versioning system.