From ab449d56c4f4ed319dd25c6e32395f8a56cd06d7 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Sun, 20 Sep 2026 01:52:19 +0530 Subject: [PATCH 1/3] fix(cursor_desktop): support delete on pre-table schemas and KV-only sessions --- src/harness/cursor_desktop.rs | 28 ++++++----- tests/integration/cursor_desktop.rs | 76 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/harness/cursor_desktop.rs b/src/harness/cursor_desktop.rs index 895c851..914bbc7 100644 --- a/src/harness/cursor_desktop.rs +++ b/src/harness/cursor_desktop.rs @@ -1138,22 +1138,26 @@ impl Store for CursorDesktopStore { fn delete(&self, reference: &String) -> Result<()> { super::checked_id_component(CursorDesktop::NAME, reference)?; let conn = Connection::open(self.db_path()).map_err(sqlite_err)?; - let deleted: usize = conn - .execute( + let deleted_headers = if has_table(&conn, "composerHeaders")? { + conn.execute( "DELETE FROM composerHeaders WHERE composerId = ?1", params![reference], ) + .map_err(sqlite_err)? + } else { + 0 + }; + let deleted_kv: usize = conn + .execute( + "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3", + params![ + format!("composerData:{reference}"), + format!("bubbleId:{reference}:%"), + format!("%:{reference}%"), + ], + ) .map_err(sqlite_err)?; - conn.execute( - "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3", - params![ - format!("composerData:{reference}"), - format!("bubbleId:{reference}:%"), - format!("%:{reference}%"), - ], - ) - .map_err(sqlite_err)?; - if deleted == 0 { + if deleted_headers == 0 && deleted_kv == 0 { return Err(Error::Malformed { harness: CursorDesktop::NAME, detail: format!("no such session: {reference}"), diff --git a/tests/integration/cursor_desktop.rs b/tests/integration/cursor_desktop.rs index 74ef991..d561b43 100644 --- a/tests/integration/cursor_desktop.rs +++ b/tests/integration/cursor_desktop.rs @@ -548,3 +548,79 @@ fn pending_tool_call_yields_no_result_message() { }] ); } + +#[cfg(feature = "opencode")] +#[test] +fn delete_roundtrip_removes_session_and_fails_on_duplicate() { + let dir = tempfile::tempdir().unwrap(); + let store = CursorDesktopStore::new(dir.path()); + let saved = store.save(&sample_transcript()).unwrap(); + + assert_eq!(store.discover().unwrap().len(), 1); + store.delete(&saved.reference).unwrap(); + assert_eq!(store.discover().unwrap().len(), 0); + + assert!(store.delete(&saved.reference).is_err()); +} + +#[cfg(feature = "opencode")] +#[test] +fn delete_succeeds_on_database_without_composer_headers_table() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("globalStorage").join("state.vscdb"); + std::fs::create_dir_all(db_path.parent().unwrap()).unwrap(); + + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute_batch( + "CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO cursorDiskKV VALUES ('composerData:pre-table-1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:pre-table-1:b1', '{}');", + ) + .unwrap(); + + let store = CursorDesktopStore::new(dir.path()); + store.delete(&"pre-table-1".to_string()).unwrap(); + + let count: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%pre-table-1%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count, 0); + + // Deleting again should fail with no such session + assert!(store.delete(&"pre-table-1".to_string()).is_err()); +} + +#[cfg(feature = "opencode")] +#[test] +fn delete_succeeds_on_kv_only_session_in_modern_database() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("globalStorage").join("state.vscdb"); + std::fs::create_dir_all(db_path.parent().unwrap()).unwrap(); + + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute_batch( + "CREATE TABLE composerHeaders (composerId TEXT PRIMARY KEY, recency INTEGER); + CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO cursorDiskKV VALUES ('composerData:kv-only-1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:kv-only-1:b1', '{}');", + ) + .unwrap(); + + let store = CursorDesktopStore::new(dir.path()); + store.delete(&"kv-only-1".to_string()).unwrap(); + + let count: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%kv-only-1%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count, 0); + + assert!(store.delete(&"kv-only-1".to_string()).is_err()); +} From 5a92dad715d5685c8930c604917ea0cdd92f8199 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:39:39 +0530 Subject: [PATCH 2/3] fix(cursor_desktop): match exact session delimiters on delete --- src/harness/cursor_desktop.rs | 5 +-- tests/integration/cursor_desktop.rs | 53 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/harness/cursor_desktop.rs b/src/harness/cursor_desktop.rs index 914bbc7..48495bf 100644 --- a/src/harness/cursor_desktop.rs +++ b/src/harness/cursor_desktop.rs @@ -1149,11 +1149,12 @@ impl Store for CursorDesktopStore { }; let deleted_kv: usize = conn .execute( - "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3", + "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3 OR key LIKE ?4", params![ format!("composerData:{reference}"), format!("bubbleId:{reference}:%"), - format!("%:{reference}%"), + format!("%:{reference}"), + format!("%:{reference}:%"), ], ) .map_err(sqlite_err)?; diff --git a/tests/integration/cursor_desktop.rs b/tests/integration/cursor_desktop.rs index d561b43..8dd5b8a 100644 --- a/tests/integration/cursor_desktop.rs +++ b/tests/integration/cursor_desktop.rs @@ -624,3 +624,56 @@ fn delete_succeeds_on_kv_only_session_in_modern_database() { assert!(store.delete(&"kv-only-1".to_string()).is_err()); } + +#[cfg(feature = "opencode")] +#[test] +fn delete_exact_session_id_does_not_delete_prefixed_sibling() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("globalStorage").join("state.vscdb"); + std::fs::create_dir_all(db_path.parent().unwrap()).unwrap(); + + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute_batch( + "CREATE TABLE composerHeaders (composerId TEXT PRIMARY KEY, recency INTEGER); + CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO composerHeaders VALUES ('session-1', 100); + INSERT INTO composerHeaders VALUES ('session-10', 200); + INSERT INTO cursorDiskKV VALUES ('composerData:session-1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:session-1:b1', '{}'); + INSERT INTO cursorDiskKV VALUES ('checkpointId:session-1:c1', '{}'); + INSERT INTO cursorDiskKV VALUES ('composerData:session-10', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:session-10:b1', '{}'); + INSERT INTO cursorDiskKV VALUES ('checkpointId:session-10:c1', '{}');", + ) + .unwrap(); + + let store = CursorDesktopStore::new(dir.path()); + store.delete(&"session-1".to_string()).unwrap(); + + let count_s10_headers: i64 = conn + .query_row( + "SELECT count(*) FROM composerHeaders WHERE composerId = 'session-10'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count_s10_headers, 1); + + let remaining_s10_keys: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%session-10%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(remaining_s10_keys, 3); + + let remaining_target_keys: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%session-1%' AND key NOT LIKE '%session-10%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(remaining_target_keys, 0); +} From f53c53a13f933458a58c6d42a7eb40b5efdaabc4 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:18:31 +0530 Subject: [PATCH 3/3] fix(cursor_desktop): escape LIKE wildcards in delete pattern --- src/harness/cursor_desktop.rs | 21 +++++++-- tests/integration/cursor_desktop.rs | 66 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/harness/cursor_desktop.rs b/src/harness/cursor_desktop.rs index d151537..488cba4 100644 --- a/src/harness/cursor_desktop.rs +++ b/src/harness/cursor_desktop.rs @@ -1290,14 +1290,15 @@ impl Store for CursorDesktopStore { } else { 0 }; + let escaped = escape_like_pattern(reference); let deleted_kv: usize = conn .execute( - "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3 OR key LIKE ?4", + "DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 ESCAPE '\\' OR key LIKE ?3 ESCAPE '\\' OR key LIKE ?4 ESCAPE '\\'", params![ format!("composerData:{reference}"), - format!("bubbleId:{reference}:%"), - format!("%:{reference}"), - format!("%:{reference}:%"), + format!("bubbleId:{escaped}:%"), + format!("%:{escaped}"), + format!("%:{escaped}:%"), ], ) .map_err(sqlite_err)?; @@ -1442,6 +1443,18 @@ fn discover_from_headers(conn: &Connection) -> Result>> { .collect()) } +#[cfg(feature = "opencode")] +fn escape_like_pattern(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + if matches!(c, '%' | '_' | '\\') { + out.push('\\'); + } + out.push(c); + } + out +} + /// Half-open key range covering every key that starts with `prefix`: the /// upper bound is the prefix with its last character stepped up by one /// code point, which sorts after every extension of the prefix under diff --git a/tests/integration/cursor_desktop.rs b/tests/integration/cursor_desktop.rs index c56ec3a..6376def 100644 --- a/tests/integration/cursor_desktop.rs +++ b/tests/integration/cursor_desktop.rs @@ -803,6 +803,72 @@ fn delete_exact_session_id_does_not_delete_prefixed_sibling() { assert_eq!(remaining_target_keys, 0); } +#[cfg(feature = "opencode")] +#[test] +fn delete_escapes_like_wildcards_in_session_id() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("globalStorage").join("state.vscdb"); + std::fs::create_dir_all(db_path.parent().unwrap()).unwrap(); + + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute_batch( + "CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO cursorDiskKV VALUES ('composerData:session_1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:session_1:b1', '{}'); + INSERT INTO cursorDiskKV VALUES ('composerData:sessionX1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:sessionX1:b1', '{}'); + INSERT INTO cursorDiskKV VALUES ('composerData:session%1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:session%1:b1', '{}'); + INSERT INTO cursorDiskKV VALUES ('composerData:sessionANYTHING1', '{}'); + INSERT INTO cursorDiskKV VALUES ('bubbleId:sessionANYTHING1:b1', '{}');", + ) + .unwrap(); + + let store = CursorDesktopStore::new(dir.path()); + + // Delete session_1: should NOT delete sessionX1 or sessionANYTHING1 or session%1 + store.delete(&"session_1".to_string()).unwrap(); + + let count_session_x1: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%sessionX1%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count_session_x1, 2); + + let count_deleted_underscore: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%session\\_1%' ESCAPE '\\'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count_deleted_underscore, 0); + + // Delete session%1: should NOT delete sessionANYTHING1 + store.delete(&"session%1".to_string()).unwrap(); + + let count_session_anything1: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%sessionANYTHING1%'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count_session_anything1, 2); + + let count_deleted_percent: i64 = conn + .query_row( + "SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%session\\%1%' ESCAPE '\\'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count_deleted_percent, 0); +} + #[cfg(feature = "opencode")] #[test] fn store_resolves_workspace_id_with_percent_encoding_and_spaces() {