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.

python
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.

Diagram

File Snapshot

🤖 Agent run

📸 track

🌑 Shadow git

📋 diff / undo / redo

Quick Start

  1. 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
  2. With Configuration

    Use AutonomyConfig for explicit control, or call FileSnapshot directly:

    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)
  3. Re-root After a Workspace

    Agent.__init__ roots change tracking at os.getcwd(). Attach a per-session directory later, then call set_snapshot_root() so undo() 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.

Diagram

⚙️ track_changes=True

🌑 Shadow git repo

📸 snapshot=True

📍 Pre-run snapshot

📋 diff(from_hash)

↩️ undo / redo / diff

FieldDefaultWhat it does
track_changesTrue for full_auto, else FalseKeeps a shadow-git repo for the agent’s workspace
snapshotFalseRecords 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.

Diagram
Shadow repoFileSnapshotAgentBot/Gateway wrapperShadow repoFileSnapshotAgentBot/Gateway wrapperundo/redo stacks clearedset_snapshot_root(workspace.root)FileSnapshot(project_path=workspace.root)track(message)commit workspace statecommit_hashedits files via toolsundo()checkout files
Diagram

🤖 Wrapper

🤖 Agent.set_snapshot_root

📸 new FileSnapshot

🌑 Shadow repo

BehaviourDetail
Path resolutionproject_path is normalised via os.path.abspath() — relative paths resolve against process cwd
No-op on same rootCalled with the current root → returns True immediately and keeps the existing manager
Re-root clears stacksCalled with a new root → creates a fresh FileSnapshot and clears the undo/redo stacks
Preserves snapshot_dirReuses autonomy_config["snapshot_dir"] if set, so a custom shadow-repo base survives re-rooting
Silent fallbackIf FileSnapshot(...) fails (git missing, permission error), returns False and logs at DEBUG — never raises
Thread-safeUses the agent’s snapshot lock when clearing the stacks
ParameterTypeDescription
project_pathstrDirectory 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:

python
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

Diagram
Shadow repoFileSnapshotAgentShadow repoFileSnapshotAgenttrack(message)commit workspace statecommit_hashedits files via toolsdiff(from_hash)git diffFileDiff listundo() / restore(hash)checkout files
ComponentRole
Shadow gitHidden 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 helpersagent.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.

Diagram

Yes

No

🤖 Agent run

git call

Returns within
60s?

✅ Continue

⏱️ Timed out

🌑 Snapshot skipped,
agent continues

GuardValueEffect
Timeout60 sRaises subprocess.TimeoutExpired instead of hanging on credential prompts, index.lock waits, hung network filesystems, or slow disks
GIT_TERMINAL_PROMPT=0Set in the git child environmentgit fails fast instead of prompting for input
Timeout during setupSame as git missingTracking 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:

CaseBehaviour
File exists in snapshot, missing in projectRestored from shadow git
File exists in project, not in snapshotRemoved — no leak from a later snapshot
File was deleted before the snapshotStays 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 restoreOnly the listed files are touched; project prune step is skipped

Common Patterns

Selective restore

python
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

python
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.


Related