feat: async captured checkpoints for bash tool hooks#842
feat: async captured checkpoints for bash tool hooks#842jwiegley wants to merge 21 commits intojohnw/bash-supportfrom
Conversation
| CheckpointRunRequest::Live(req) => req | ||
| .agent_run_result | ||
| .as_ref() | ||
| .and_then(|r| r.edited_filepaths.clone()) | ||
| .unwrap_or_default(), |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
dc33b42 to
9f8b8bb
Compare
a083a23 to
7fe3640
Compare
9f8b8bb to
82fc9dd
Compare
5dcd157 to
d123310
Compare
| 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, | ||
| }; |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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>
c3369d4 to
059b508
Compare
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>
2c32e66 to
9bf7fd4
Compare
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>
Summary
snapshot.watermarksAPI to query per-file mtime watermarksHumancheckpointAiAgentcheckpointDesign
See
docs/superpowers/specs/2026-03-27-async-bash-snapshots-design.mdChanges
SnapshotWatermarkscontrol API with 500ms timeoutFamilyState.file_snapshot_watermarkswith#[serde(default)]for backward compatBashToolResultwrapper type replacing rawBashCheckpointActionreturnscaptured_checkpoint_idfield onAgentRunResultfor early-return handler pathTest plan
system_time_to_nanos,find_stale_files,capture_file_contentsbash_tool_provenanceintegration tests passcargo testsuite passes (1312 lib tests)cargo clippy --all-targetsclean🤖 Generated with Claude Code