feat: major /rollback improvements — enabled by default, diff preview, file-level restore, conversation undo, terminal checkpoints
Checkpoint & rollback upgrades: 1. Enabled by default — checkpoints are now on for all new sessions. Zero cost when no file-mutating tools fire. Disable with checkpoints.enabled: false in config.yaml. 2. Diff preview — /rollback diff <N> shows a git diff between the checkpoint and current working tree before committing to a restore. 3. File-level restore — /rollback <N> <file> restores a single file from a checkpoint instead of the entire directory. 4. Conversation undo on rollback — when restoring files, the last chat turn is automatically undone so the agent's context matches the restored filesystem state. 5. Terminal command checkpoints — destructive terminal commands (rm, mv, sed -i, truncate, git reset/clean, output redirects) now trigger automatic checkpoints before execution. Previously only write_file and patch were covered. 6. Change summary in listing — /rollback now shows file count and +insertions/-deletions for each checkpoint. 7. Fixed dead code — removed duplicate _run_git call in list_checkpoints with nonsensical --all if False condition. 8. Updated help text — /rollback with no args now shows available subcommands (diff, file-level restore).
This commit is contained in:
parent
00a0c56598
commit
9e845a6e53
4 changed files with 237 additions and 44 deletions
51
run_agent.py
51
run_agent.py
|
|
@ -205,6 +205,33 @@ _NEVER_PARALLEL_TOOLS = frozenset({"clarify"})
|
|||
# Maximum number of concurrent worker threads for parallel tool execution.
|
||||
_MAX_TOOL_WORKERS = 8
|
||||
|
||||
# Patterns that indicate a terminal command may modify/delete files.
|
||||
_DESTRUCTIVE_PATTERNS = re.compile(
|
||||
r"""(?:^|\s|&&|\|\||;|`)(?:
|
||||
rm\s|rmdir\s|
|
||||
mv\s|
|
||||
sed\s+-i|
|
||||
truncate\s|
|
||||
dd\s|
|
||||
shred\s|
|
||||
git\s+(?:reset|clean|checkout)\s
|
||||
)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
# Output redirects that overwrite files (> but not >>)
|
||||
_REDIRECT_OVERWRITE = re.compile(r'[^>]>[^>]|^>[^>]')
|
||||
|
||||
|
||||
def _is_destructive_command(cmd: str) -> bool:
|
||||
"""Heuristic: does this terminal command look like it modifies/deletes files?"""
|
||||
if not cmd:
|
||||
return False
|
||||
if _DESTRUCTIVE_PATTERNS.search(cmd):
|
||||
return True
|
||||
if _REDIRECT_OVERWRITE.search(cmd):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _inject_honcho_turn_context(content, turn_context: str):
|
||||
"""Append Honcho recall to the current-turn user message without mutating history.
|
||||
|
|
@ -3842,6 +3869,18 @@ class AIAgent:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Checkpoint before destructive terminal commands
|
||||
if function_name == "terminal" and self._checkpoint_mgr.enabled:
|
||||
try:
|
||||
cmd = function_args.get("command", "")
|
||||
if _is_destructive_command(cmd):
|
||||
cwd = function_args.get("workdir") or os.getenv("TERMINAL_CWD", os.getcwd())
|
||||
self._checkpoint_mgr.ensure_checkpoint(
|
||||
cwd, f"before terminal: {cmd[:60]}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parsed_calls.append((tool_call, function_name, function_args))
|
||||
|
||||
# ── Logging / callbacks ──────────────────────────────────────────
|
||||
|
|
@ -4035,6 +4074,18 @@ class AIAgent:
|
|||
except Exception:
|
||||
pass # never block tool execution
|
||||
|
||||
# Checkpoint before destructive terminal commands
|
||||
if function_name == "terminal" and self._checkpoint_mgr.enabled:
|
||||
try:
|
||||
cmd = function_args.get("command", "")
|
||||
if _is_destructive_command(cmd):
|
||||
cwd = function_args.get("workdir") or os.getenv("TERMINAL_CWD", os.getcwd())
|
||||
self._checkpoint_mgr.ensure_checkpoint(
|
||||
cwd, f"before terminal: {cmd[:60]}"
|
||||
)
|
||||
except Exception:
|
||||
pass # never block tool execution
|
||||
|
||||
tool_start_time = time.time()
|
||||
|
||||
if function_name == "todo":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue