File Snapshot Module
Shadow git repository for file change tracking and one-click undo
Track file changes in a hidden git repo so agents can snapshot, diff, and restore workspace files without touching your real repository.
from praisonaiagents import Agent, AutonomyConfig
agent = Agent(
name="coder",
instructions="Snapshot files before autonomous edits",
autonomy=AutonomyConfig(track_changes=True, snapshot=True),
)
agent.start("Refactor the module")The user approves autonomous edits; shadow-git snapshots let the agent diff, undo, or redo workspace changes safely.
Quick Start
- Simple Usage
Turn on change tracking and the pre-run snapshot so
undo()has something to restore:python from praisonaiagents import Agent, AutonomyConfig agent = Agent( name="Refactorer", instructions="Edit files carefully.", autonomy=AutonomyConfig(level="full_auto", track_changes=True, snapshot=True), ) agent.start("Rename getUserName to getUserEmail in src/user.js") agent.diff() # see what changed agent.undo() # restore pre-run state - With Configuration
Use
AutonomyConfigfor explicit control, or callFileSnapshotdirectly:python from praisonaiagents import Agent from praisonaiagents import AutonomyConfig from praisonaiagents.snapshot import FileSnapshot agent = Agent( name="Editor", autonomy=AutonomyConfig(level="full_auto", track_changes=True, snapshot=True), ) # Or standalone — no agent required snapshot = FileSnapshot(project_path=".") info = snapshot.track(message="Before refactor") # ... edit files ... snapshot.restore(info.commit_hash) - Re-root After a Workspace
Agent.__init__roots change tracking atos.getcwd(). Attach a per-session directory later, then callset_snapshot_root()soundo()restores the right place:python from praisonaiagents import Agent, AutonomyConfig agent = Agent( name="Editor", instructions="Edit files carefully.", autonomy=AutonomyConfig(track_changes=True, snapshot=True), ) # Later — once you know the workspace the tools will actually write to agent.set_snapshot_root("/tmp/session-42/workspace") agent.start("Rename getUserName in src/user.js") agent.undo() # restores /tmp/session-42/workspace, not process cwd
snapshot vs track_changes
The two fields are independent — undo() needs both.
| Field | Default | What it does |
|---|---|---|
track_changes | True for full_auto, else False | Keeps a shadow-git repo for the agent’s workspace |
snapshot | False | Records one pre-run snapshot in run_autonomous(). undo() restores it; diff() with no argument compares against it |
Re-rooting Change Tracking
set_snapshot_root(project_path) re-roots undo/redo/diff at a directory known only after construction — the moment a bot, gateway, or custom wrapper attaches a per-chat Workspace.
| Behaviour | Detail |
|---|---|
| Path resolution | project_path is normalised via os.path.abspath() — relative paths resolve against process cwd |
| No-op on same root | Called with the current root → returns True immediately and keeps the existing manager |
| Re-root clears stacks | Called with a new root → creates a fresh FileSnapshot and clears the undo/redo stacks |
Preserves snapshot_dir | Reuses autonomy_config["snapshot_dir"] if set, so a custom shadow-repo base survives re-rooting |
| Silent fallback | If FileSnapshot(...) fails (git missing, permission error), returns False and logs at DEBUG — never raises |
| Thread-safe | Uses the agent’s snapshot lock when clearing the stacks |
| Parameter | Type | Description |
|---|---|---|
project_path | str | Directory to root change tracking at. Absolute-normalised; relative paths resolve against cwd. |
Returns bool — True when a snapshot manager is rooted at the given path, False if construction failed (e.g. git unavailable).
Bot / Gateway Workspaces
Bots and gateways call Agent.set_snapshot_root(workspace.root) from apply_bot_smart_defaults() immediately after attaching the per-chat Workspace. So /undo in a Slack, Discord, or Telegram chat reverts files in that chat’s workspace — never the gateway’s process cwd.
Custom wrappers that attach a workspace after Agent.__init__ should do the same:
from praisonaiagents import Agent, AutonomyConfig
agent = Agent(name="assistant", autonomy=AutonomyConfig(track_changes=True, snapshot=True))
# ... attach your workspace ...
agent.set_snapshot_root(str(workspace.root))See Bot Gateway for how the gateway wires this automatically.
How It Works
| Component | Role |
|---|---|
| Shadow git | Hidden repo under ~/.praisonai/snapshots/ (honours PRAISONAI_HOME) — never affects your project git |
track() | Commits current file state; returns SnapshotInfo with hash |
diff() | Compares two snapshots or snapshot vs working tree |
restore() | Checks out files from a snapshot; optional file list for partial restore |
| Agent helpers | agent.undo(), agent.redo(), agent.diff() when track_changes=True and snapshot=True |
Git Subprocess Guard
Every git call made by FileSnapshot and the GitWorktreeAdapter is time-limited and non-interactive, so a stuck git can never block an agent turn.
| Guard | Value | Effect |
|---|---|---|
| Timeout | 60 s | Raises subprocess.TimeoutExpired instead of hanging on credential prompts, index.lock waits, hung network filesystems, or slow disks |
GIT_TERMINAL_PROMPT=0 | Set in the git child environment | git fails fast instead of prompting for input |
| Timeout during setup | Same as git missing | Tracking is skipped and undo() returns False, as described above |
Added in PraisonAI PR #6172. The Gateway Subprocess Guard uses the same pattern.
Restore Guarantees
restore() (and agent.undo()) rebuilds the project to exactly the target snapshot’s tree:
| Case | Behaviour |
|---|---|
| File exists in snapshot, missing in project | Restored from shadow git |
| File exists in project, not in snapshot | Removed — no leak from a later snapshot |
| File was deleted before the snapshot | Stays deleted — deletion is recorded, not resurrected |
File is ignored (.env, venv, node_modules, .gitignore matches) | Preserved untouched — the snapshot never tracked it, so restore never prunes it |
files=[...] selective restore | Only the listed files are touched; project prune step is skipped |
Common Patterns
Selective restore
from praisonaiagents.snapshot import FileSnapshot
snapshot = FileSnapshot(project_path="/my/project")
initial = snapshot.track(message="Checkpoint")
# ... changes across many files ...
snapshot.restore(initial.commit_hash, files=["src/config.py", "src/utils.py"])List recent snapshots
for info in snapshot.list_snapshots(limit=10):
print(f"{info.commit_hash[:8]} — {info.message} ({info.files_changed} files)")Best Practices
Prefer agent undo for autonomous runs
With AutonomyConfig(level="full_auto", snapshot=True), call agent.undo() rather than manual restore() — the agent maintains an undo/redo stack across iterations.
Snapshot before risky edits
Call track(message=...) (or run the agent once) before bulk refactors so you have a named rollback point.
Respects .gitignore
The shadow repo honours .gitignore patterns plus built-in defaults (.git, __pycache__, node_modules, venv, .venv, .pytest_cache, .mypy_cache). Ignored files are neither snapshotted nor pruned by restore(). A .env file that lives outside version control survives every undo() / restore().
Clean up when finished
Call snapshot.cleanup() to remove the shadow repository if you no longer need history for a project.
Re-root after attaching a workspace
Agent.__init__ roots the snapshot at os.getcwd(). If you attach a Workspace (or any per-session root) later, call agent.set_snapshot_root(str(workspace.root)) immediately — otherwise agent.undo() restores files in the wrong directory. Re-rooting clears the undo/redo stacks; rooting at the current directory is a no-op.