Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/mark/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,33 @@ fn create_image_from_data(
Ok(image)
}

/// Cancel and delete any reviews (auto or manual) created at or after a commit's
/// `created_at` timestamp. If a review has an active session, the session is
/// cancelled via the registry and then deleted.
fn cleanup_reviews_after_commit(
store: &Arc<Store>,
registry: &session_runner::SessionRegistry,
commit: &store::models::Commit,
) {
// Only clean up reviews for commits that actually landed (have a SHA).
// Pending/failed commits without a SHA never produced a reviewable diff.
if commit.sha.is_none() {
return;
}

let reviews = match store.find_reviews_created_since(&commit.branch_id, commit.created_at) {

Choose a reason for hiding this comment

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

P1 Badge Restrict review cleanup to the deleted commit

cleanup_reviews_after_commit selects reviews only by branch_id and created_at >= commit.created_at, so deleting a pending commit (or deleting HEAD after later manual reviews were added) will also delete unrelated reviews and their sessions on the same branch. For example, a manual review created later for an older surviving commit is still removed because it matches the timestamp window, which causes user data loss beyond the commit being deleted.

Useful? React with 👍 / 👎.

Ok(r) => r,
Err(_) => return,
};
for review in reviews {
if let Some(ref sid) = review.session_id {
registry.cancel(sid);
let _ = store.delete_session(sid);
}
let _ = store.delete_review(&review.id);
}
}

/// Delete a review and all its comments, optionally deleting its linked session.
#[tauri::command(rename_all = "camelCase")]
fn delete_review(
Expand Down Expand Up @@ -1435,6 +1462,7 @@ fn delete_review(
/// This does NOT touch git — it only removes the DB record and optionally its session.
#[tauri::command(rename_all = "camelCase")]
fn delete_pending_commit(
registry: tauri::State<'_, Arc<session_runner::SessionRegistry>>,
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
commit_id: String,
delete_session: Option<bool>,
Expand All @@ -1454,6 +1482,9 @@ fn delete_pending_commit(
);
}

// Clean up reviews created at or after this commit
cleanup_reviews_after_commit(&store, &registry, &commit);

store.delete_commit(&commit_id).map_err(|e| e.to_string())?;

if delete_session.unwrap_or(false) {
Expand All @@ -1472,6 +1503,7 @@ fn delete_pending_commit(
/// Returns an error if the commit is not the current HEAD.
#[tauri::command(rename_all = "camelCase")]
fn delete_commit(
registry: tauri::State<'_, Arc<session_runner::SessionRegistry>>,
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
branch_id: String,
commit_sha: String,
Expand Down Expand Up @@ -1510,6 +1542,9 @@ fn delete_commit(

// Clean up DB record if one exists
if let Ok(Some(db_commit)) = store.get_commit_by_sha(&branch_id, &commit_sha) {
// Clean up reviews created at or after this commit
cleanup_reviews_after_commit(&store, &registry, &db_commit);

let _ = store.delete_commit(&db_commit.id);

if delete_session.unwrap_or(false) {
Expand Down
27 changes: 25 additions & 2 deletions apps/mark/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,29 @@ pub fn resume_session(
.flatten()
.map(|note| note.project_id);

// If this session is linked to a commit, capture the current HEAD so we
// can detect amended commits when the session completes.
let (pre_head_sha, workspace_name) =
if let Ok(Some(commit)) = store.get_commit_by_session(&session_id) {
// Only bother if the commit already has a SHA (i.e. was previously completed).
if commit.sha.is_some() {
let branch = store.get_branch(&commit.branch_id).ok().flatten();
let ws_name = branch.as_ref().and_then(|b| b.workspace_name.clone());
let head = if let Some(ref ws) = ws_name {
crate::blox::ws_exec(ws, &["git", "rev-parse", "HEAD"])
.map(|s| s.trim().to_string())
.ok()
} else {
crate::git::get_head_sha(&working_dir).ok()
};
(head, ws_name)
} else {
(None, None)
}
} else {
(None, None)
};

let transitioned = store
.transition_to_running(&session_id)
.map_err(|e| e.to_string())?;
Expand All @@ -238,9 +261,9 @@ pub fn resume_session(
prompt,
working_dir,
agent_session_id,
pre_head_sha: None,
pre_head_sha,
provider,
workspace_name: None,
workspace_name,
extra_env: vec![],
mcp_project_id: mcp_project_id.clone(),
action_executor: if mcp_project_id.is_some() {
Expand Down
14 changes: 10 additions & 4 deletions apps/mark/src-tauri/src/session_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,9 @@ fn run_post_completion_hooks(

// --- Commit detection ---
if let Some(pre_sha) = pre_head_sha {
if let Ok(Some(pending_commit)) = store.get_pending_commit_by_session(session_id) {
// Look for any commit linked to this session — not just pending (sha IS NULL)
// ones — so we also detect amended commits on resumed sessions.
if let Ok(Some(commit)) = store.get_commit_by_session(session_id) {
// Get current HEAD — either from local worktree or remote workspace.
let current_head_result = if let Some(ws_name) = workspace_name {
crate::blox::ws_exec(ws_name, &["git", "rev-parse", "HEAD"])
Expand All @@ -521,13 +523,17 @@ fn run_post_completion_hooks(
&pre_sha[..7.min(pre_sha.len())],
&current_head[..7.min(current_head.len())]
);
if let Err(e) = store.update_commit_sha(&pending_commit.id, &current_head) {
if let Err(e) = store.update_commit_sha(&commit.id, &current_head) {
log::error!("Failed to update commit SHA: {e}");
}
committed_branch_id = Some(pending_commit.branch_id.clone());
committed_branch_id = Some(commit.branch_id.clone());
}
Ok(_) => {
log::info!("Session {session_id}: no new commit (HEAD unchanged), leaving pending commit as failed");
if commit.sha.is_none() {
log::info!("Session {session_id}: no new commit (HEAD unchanged), leaving pending commit as failed");
} else {
log::info!("Session {session_id}: no commit change (HEAD unchanged)");
}
}
Err(e) => {
log::error!("Failed to get HEAD SHA after session: {e}");
Expand Down
9 changes: 3 additions & 6 deletions apps/mark/src-tauri/src/store/commits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,12 @@ impl Store {
Ok(())
}

/// Find a pending commit (sha IS NULL) linked to a given session.
pub fn get_pending_commit_by_session(
&self,
session_id: &str,
) -> Result<Option<Commit>, StoreError> {
/// Find any commit linked to a given session (regardless of SHA status).
pub fn get_commit_by_session(&self, session_id: &str) -> Result<Option<Commit>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, branch_id, sha, session_id, created_at, updated_at
FROM commits WHERE session_id = ?1 AND sha IS NULL",
FROM commits WHERE session_id = ?1",
params![session_id],
Self::row_to_commit,
)
Expand Down
26 changes: 25 additions & 1 deletion apps/mark/src-tauri/src/store/reviews.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,25 @@ impl Store {
Ok(())
}

/// Find all reviews on a branch created at or after a given timestamp.
/// Returns just the review headers (no children loaded) since callers
/// typically only need the id and session_id for cleanup.
pub fn find_reviews_created_since(
&self,
branch_id: &str,
since: i64,
) -> Result<Vec<Review>, StoreError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, branch_id, commit_sha, scope, session_id, title, is_auto, created_at, updated_at
FROM reviews
WHERE branch_id = ?1 AND created_at >= ?2
ORDER BY created_at ASC",
)?;
let rows = stmt.query_map(params![branch_id, since], Self::row_to_review_header)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}

/// Update the title of a review.
pub fn update_review_title(&self, id: &str, title: &str) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
Expand Down Expand Up @@ -334,14 +353,19 @@ impl Store {
}
}

/// Find the most recent auto review for a branch.
/// Find the most recent auto review for a branch, but only if it was
/// created after every commit on the branch. This prevents stale auto
/// reviews (e.g. from before an amended commit) from surfacing.
pub fn find_latest_auto_review(&self, branch_id: &str) -> Result<Option<Review>, StoreError> {
let conn = self.conn.lock().unwrap();
let review: Option<Review> = conn
.query_row(
"SELECT id, branch_id, commit_sha, scope, session_id, title, is_auto, created_at, updated_at
FROM reviews
WHERE branch_id = ?1 AND is_auto = 1
AND created_at >= COALESCE(
(SELECT MAX(updated_at) FROM commits WHERE branch_id = ?1 AND sha IS NOT NULL),
0)
ORDER BY created_at DESC LIMIT 1",
params![branch_id],
Self::row_to_review_header,
Expand Down
Loading