Skip to content

feat: async captured checkpoints for bash tool hooks#842

Open
jwiegley wants to merge 21 commits intojohnw/bash-supportfrom
johnw/async-bash-snapshots
Open

feat: async captured checkpoints for bash tool hooks#842
jwiegley wants to merge 21 commits intojohnw/bash-supportfrom
johnw/async-bash-snapshots

Conversation

@jwiegley
Copy link
Copy Markdown
Collaborator

@jwiegley jwiegley commented Mar 27, 2026

Summary

  • Adds daemon snapshot.watermarks API to query per-file mtime watermarks
  • Pre-hook captures content of stale files (mtime > watermark + grace) as Human checkpoint
  • Post-hook captures bash command's changed files as AiAgent checkpoint
  • Both submitted as fire-and-forget captured checkpoints via existing pipeline
  • Graceful degradation: non-daemon mode, query failures, large trees all fall back safely

Design

See docs/superpowers/specs/2026-03-27-async-bash-snapshots-design.md

Changes

  • 17 files changed, 853 insertions across daemon infrastructure, bash tool, presets, and handler
  • New SnapshotWatermarks control API with 500ms timeout
  • FamilyState.file_snapshot_watermarks with #[serde(default)] for backward compat
  • BashToolResult wrapper type replacing raw BashCheckpointAction returns
  • captured_checkpoint_id field on AgentRunResult for early-return handler path
  • 8 new unit tests + 2 family actor watermark tests

Test plan

  • Unit tests for system_time_to_nanos, find_stale_files, capture_file_contents
  • Family actor watermark round-trip tests (get/update/monotonic advance)
  • All 54 bash_tool_provenance integration tests pass
  • Full cargo test suite passes (1312 lib tests)
  • cargo clippy --all-targets clean

🤖 Generated with Claude Code


Open with Devin

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +4831 to +4835
CheckpointRunRequest::Live(req) => req
.agent_run_result
.as_ref()
.and_then(|r| r.edited_filepaths.clone())
.unwrap_or_default(),
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Live checkpoint watermark computation ignores will_edit_filepaths for Human checkpoints

In src/daemon.rs:5257-5261, when computing checkpoint_file_paths for watermark updates on the CheckpointRunRequest::Live path, only edited_filepaths is read. Human checkpoints store their target paths in will_edit_filepaths (not edited_filepaths), as seen in src/commands/checkpoint.rs:183-187 (explicit_capture_target_paths selects will_edit_filepaths for Human kind). This means any Human checkpoint processed via the Live daemon path will produce an empty checkpoint_file_paths, causing compute_watermarks_from_stat at src/daemon.rs:5282 to return no watermarks. Consequently, the watermark map is never updated for these files, and find_stale_files (src/commands/checkpoint_agent/bash_tool.rs:849-876) will perpetually treat them as stale (no watermark → always stale), causing unnecessary pre-hook captures on every subsequent bash tool invocation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@jwiegley jwiegley force-pushed the johnw/bash-support branch from dc33b42 to 9f8b8bb Compare March 28, 2026 00:33
@jwiegley jwiegley force-pushed the johnw/async-bash-snapshots branch from a083a23 to 7fe3640 Compare March 28, 2026 00:34
@jwiegley jwiegley force-pushed the johnw/bash-support branch from 9f8b8bb to 82fc9dd Compare March 31, 2026 22:22
@jwiegley jwiegley force-pushed the johnw/async-bash-snapshots branch 3 times, most recently from 5dcd157 to d123310 Compare April 1, 2026 16:20
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1013 to +1027
let agent_run_result = AgentRunResult {
agent_id: AgentId {
tool: "bash-tool".to_string(),
id: "post-hook".to_string(),
model: String::new(),
},
agent_metadata: None,
checkpoint_kind: CheckpointKind::AiAgent,
transcript: None,
repo_working_dir: Some(repo_working_dir.clone()),
edited_filepaths: Some(changed_paths.to_vec()),
will_edit_filepaths: None,
dirty_files: Some(contents),
captured_checkpoint_id: None,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bash tool captured checkpoints lose real agent identity, transcript, and metadata

When the bash tool's attempt_post_hook_capture (bash_tool.rs:1012-1027) prepares a captured checkpoint, it builds a synthetic AgentRunResult with agent_id: { tool: "bash-tool", id: "post-hook", model: "" } and transcript: None. This captured checkpoint's ID is then propagated via captured_checkpoint_id in the real preset's AgentRunResult (e.g., Claude's result with the correct agent identity and transcript). However, when run_checkpoint_via_daemon_or_local (git_ai_handlers.rs:1092-1112) detects a captured_checkpoint_id, it submits only the capture_id and repo_working_dir as a CapturedCheckpointRunRequest, completely discarding the real AgentRunResult. When the daemon executes this captured checkpoint via execute_captured_checkpoint (checkpoint.rs:1025-1035), it uses manifest.agent_run_result — the synthetic bash-tool identity — for authorship attribution. The real AI agent's identity, transcript, and metadata are permanently lost, undermining the core purpose of AI code provenance tracking.

Prompt for agents
The fix requires passing the real agent context into attempt_post_hook_capture so the captured checkpoint manifest stores the correct agent identity and transcript. Specifically:

1. In src/commands/checkpoint_agent/bash_tool.rs, change attempt_post_hook_capture to accept additional parameters for the real agent_id, transcript, and author (or accept the full AgentRunResult). Use these when building the synthetic AgentRunResult at lines 1013-1027, replacing the hardcoded bash-tool/post-hook/empty values.

2. Alternatively, in src/commands/git_ai_handlers.rs at lines 1092-1112, when the early captured checkpoint path is taken, re-write the captured checkpoint manifest on disk to replace the synthetic AgentRunResult with the real one from the preset before submitting to the daemon. This approach avoids changing the bash_tool API but adds disk I/O.

3. A third approach: instead of using CapturedCheckpointRunRequest for bash-tool captures, submit a Live request that includes the real agent_run_result but references the pre-captured file contents. This would require extending the checkpoint execution to support a hybrid mode.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

jwiegley and others added 2 commits April 1, 2026 13:34
Add a new HashMap<String, u128> field to track per-file snapshot
watermarks (mtime in nanoseconds) for async bash checkpoint support.
Initialize the field to empty in all FamilyState constructors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new FamilyMsg::GetWatermarks variant with a oneshot response
channel, a watermarks() method on FamilyActorHandle following the
same request/response pattern as status(), and the corresponding
handler in the actor loop that returns cloned watermark state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jwiegley jwiegley force-pushed the johnw/bash-support branch from c3369d4 to 059b508 Compare April 1, 2026 20:34
jwiegley and others added 17 commits April 1, 2026 13:34
Add snapshot.watermarks variant to ControlRequest, route it through
the coordinator to the family actor's watermarks() method, and return
the result as a JSON response. Use a 500ms timeout for this lightweight
state query to keep daemon responsiveness high.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add FamilyMsg::UpdateWatermarks to accept a batch of path->mtime_ns
entries and merge them into the actor's watermark map, only advancing
the mtime when the new value is strictly greater than the existing one.
Includes a test that verifies insert, retrieval, overwrite with higher
mtime, and preservation of unaffected entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the wrapper types and helper functions needed for async bash
checkpoints: BashToolResult pairs a BashCheckpointAction with optional
CapturedCheckpointInfo, system_time_to_nanos converts SystemTime for
watermark comparison, and capture_file_contents reads file contents
while skipping binary/large/unreadable files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wraps every Ok(BashCheckpointAction::*) return in BashToolResult with
captured_checkpoint: None. This intentionally breaks callers in the
preset files; those are updated in the next commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 6 preset post-hook match blocks now bind the handle_bash_tool
result as bash_result and extract .action for the checkpoint decision.
Test helper functions in bash_tool_provenance.rs accept &BashToolResult
and inline matches! in both test files use .action field access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add query_daemon_watermarks() to contact the daemon for per-file mtime
watermarks with graceful degradation on failure, and find_stale_files()
to compare snapshot entries against watermarks for pre-hook content
capture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add attempt_pre_hook_capture() that queries daemon watermarks, finds
stale files, captures their contents, and prepares a captured checkpoint
with CheckpointKind::Human. Wire it into the PreToolUse arm so the
pre-snapshot now also produces a captured_checkpoint when stale files
are detected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire attempt_post_hook_capture into the PostToolUse stat-diff success
path so that changed file contents are captured immediately after a
bash command modifies the working tree. Uses CheckpointKind::AiAgent
with edited_filepaths, mirroring the pre-hook pattern but for the
post-execution snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add an optional captured_checkpoint_id field to AgentRunResult to carry
pre-prepared checkpoint IDs from the bash tool through to the handler,
enabling the early-return captured checkpoint submission path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…esets

Extract the captured_checkpoint_id from the bash tool result in all
presets that handle bash tools (Claude, Gemini, ContinueCli, Droid,
Amp, OpenCode) and pass it through to AgentRunResult. This enables
the handler to detect pre-prepared checkpoints and submit them via
the early-return path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the bash tool has already captured a checkpoint (via
captured_checkpoint_id in AgentRunResult), submit it directly to the
daemon before falling through to the normal capture/live checkpoint
path. On success, returns immediately with queued=true. On failure,
cleans up the captured checkpoint and falls through to the standard
checkpoint flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 8 new tests covering system_time_to_nanos, find_stale_files
(empty watermarks, grace window boundary, beyond grace window,
nonexistent files), and capture_file_contents (text read, missing
file skip).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…captures

Two critical issues from code review:

1. Watermarks were never updated in production — the UpdateWatermarks message
   existed but no code path called it. Now the daemon sequencer stats processed
   files after each checkpoint and pushes watermarks to the family actor.

2. Pre-hook captured checkpoints were prepared but never submitted — all 6
   preset sites discarded the pre-hook result with `let _ = ...`. Now the
   capture_id is wired through to the early-return AgentRunResult.

Also removes unnecessary #[allow(dead_code)] on public types and makes
load_captured_checkpoint_manifest pub(crate) for daemon access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The file contained duplicate impl AgentCheckpointPreset blocks for
GeminiPreset and ContinueCliPreset that used the old BashCheckpointAction
return type instead of BashToolResult, and were missing the
captured_checkpoint_id field on AgentRunResult. Remove the duplicates
(~1287 lines) and add the missing field to the canonical impls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove duplicate impl AgentCheckpointPreset blocks for GeminiPreset
and ContinueCliPreset. Fix the original impls to use BashToolResult
(matching on .action) instead of raw BashCheckpointAction, and add
the missing captured_checkpoint_id field to all AgentRunResult literals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the global git config is a read-only symlink (e.g. managed by
Nix/home-manager as a Nix store path), `git config --global
--remove-section trace2` fails with exit code 255. This caused
install-hooks to fail entirely during system activation.

Make trace2 configuration best-effort: log a warning instead of
aborting, since it is not required for hook installation to succeed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jwiegley jwiegley force-pushed the johnw/async-bash-snapshots branch from 2c32e66 to 9bf7fd4 Compare April 1, 2026 20:35
The two prepare_captured_checkpoint call sites in bash_tool.rs passed
a `false` reset argument that doesn't exist in the function signature,
shifting all subsequent arguments to the wrong positions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant