Conversation
Implements the single-item deletion path from issue #87. Shows item details in a dry-run by default; pass --confirm to actually delete. Reuses the existing MemoryDb::delete() which cleans up embeddings, edges, and vector data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
48bdd72 to
2165a0e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2165a0e9e8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
crates/cli/src/main.rs
Outdated
| "salience": item.salience, | ||
| "created_at": item.created_at, | ||
| "tags": item.tags, | ||
| "text_preview": &item.text[..std::cmp::min(120, item.text.len())] |
There was a problem hiding this comment.
Truncate preview text on char boundaries
The preview uses a byte slice (&item.text[..min(120, item.text.len())]), which can panic when the 120th byte falls inside a multi-byte UTF-8 character. Any memory containing non-ASCII text (e.g., emoji/CJK) around that cutoff will cause wagl forget <id> to crash before returning output or deleting, even with --confirm. Please build text_preview using a character-boundary-safe truncation method instead of byte indexing.
Useful? React with 👍 / 👎.
crates/cli/src/main.rs
Outdated
| db.delete(&id).await?; | ||
| print_ok(json_mode, serde_json::json!({ | ||
| "deleted": true, | ||
| "item": preview | ||
| }))?; |
There was a problem hiding this comment.
Unchecked delete return value — can falsely confirm deletion
db.delete(&id) returns anyhow::Result<u64> where the u64 is the number of affected rows. The result is awaited and the error is propagated, but the affected-row count is silently discarded. If the item is deleted between the db.get check and this call (concurrent process / script), affected will be 0, yet the code still prints "deleted": true.
Because this is a destructive operation, the response should verify that a row was actually removed before claiming success:
| db.delete(&id).await?; | |
| print_ok(json_mode, serde_json::json!({ | |
| "deleted": true, | |
| "item": preview | |
| }))?; | |
| let affected = db.delete(&id).await?; | |
| if affected == 0 { | |
| if json_mode { | |
| return Err(anyhow::Error::new(CliError::NotFound(format!( | |
| "memory item not found (concurrent deletion?): {}", | |
| id | |
| )))); | |
| } else { | |
| print_ok(json_mode, serde_json::json!({ | |
| "deleted": false, | |
| "id": id, | |
| "hint": "item may have been deleted concurrently" | |
| }))?; | |
| return Ok(()); | |
| } | |
| } | |
| print_ok(json_mode, serde_json::json!({ | |
| "deleted": true, | |
| "item": preview | |
| }))?; |
crates/cli/src/main.rs
Outdated
| "salience": item.salience, | ||
| "created_at": item.created_at, | ||
| "tags": item.tags, | ||
| "text_preview": &item.text[..std::cmp::min(120, item.text.len())] |
There was a problem hiding this comment.
Byte-index slice on UTF-8 string will panic on multibyte characters
&item.text[..std::cmp::min(120, item.text.len())] slices the string at byte position 120. If item.text contains multibyte UTF-8 characters (e.g. accented letters, CJK, emoji), byte offset 120 may land in the middle of a code point, causing a runtime panic: "byte index N is not a char boundary".
The same pre-existing pattern appears at lines 1908, 3099–3100, 3121, 3159, and 3683, but this PR introduces a new instance of it.
Use character-aware slicing to avoid the panic:
| "text_preview": &item.text[..std::cmp::min(120, item.text.len())] | |
| "text_preview": item.text.chars().take(120).collect::<String>() |
| let Some(item) = item else { | ||
| if json_mode { | ||
| return Err(anyhow::Error::new(CliError::NotFound(format!( | ||
| "memory item not found: {}", | ||
| id | ||
| )))); | ||
| } else { | ||
| print_ok(json_mode, serde_json::json!({ | ||
| "found": false, | ||
| "id": id | ||
| }))?; | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Inconsistent exit code when item is not found in non-JSON mode
When the item is missing and json_mode = true, a CliError::NotFound is returned, which the top-level handler maps to exit code 2. When json_mode = false, the code calls print_ok(false, …) and returns Ok(()), which exits with code 0.
For a destructive command like forget, scripts that do not use --json and rely on exit-code semantics will silently receive a success status even when the target item never existed. Emitting a user-readable message to stderr and using a non-zero exit in this branch (consistent with how Cmd::Sync handles misconfiguration) would make the behaviour match callers' expectations.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2c97e7b455
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
crates/cli/src/main.rs
Outdated
| if affected == 0 { | ||
| if !json_mode { | ||
| eprintln!("Error: memory item {} disappeared before deletion", id); | ||
| } | ||
| return Err(anyhow::anyhow!("item disappeared before deletion")); |
There was a problem hiding this comment.
Return NOT_FOUND on concurrent disappear in forget
When db.delete(&id) reports affected == 0 (the item was removed between the initial get and the delete), this branch returns a generic anyhow! error instead of CliError::NotFound. That causes wagl --json forget ... --confirm to emit RUNTIME_ERROR with exit code 1 instead of the stable NOT_FOUND/2 contract used for missing IDs, which can break automation that keys off not-found semantics in race conditions.
Useful? React with 👍 / 👎.
wagl forget <id> command to delete memories by ID
Summary
Implements
wagl forget <id>command (issue #87) — deletes a memory from the DB by its ID.Changes
wagl-db: Addeddelete_item(id)method toMemoryDbwagl-cli: Addedforgetsubcommand with:wagl forget <id>— dry-run preview by defaultwagl forget <id> --confirm— actually deleteswagl forget <id> --json— JSON output for scriptingwagl-server: Minor cleanupTesting
cargo buildpasses cleanCloses #87
PR Review by Greptile
Greptile Summary
This PR adds a
wagl forget <id>command that previews (dry-run by default) and deletes a memory item plus its associated embeddings, edges, and vector entries. The CLI plumbing is well-structured and follows existing patterns, but three correctness issues were found in the new handler.Key issues:
db.delete(&id)(number of rows affected) is discarded. If the item is concurrently deleted between thedb.getcheck and the delete call, the handler still prints"deleted": truewith exit code0, which is incorrect for a destructive command.&item.text[..std::cmp::min(120, item.text.len())]uses byte offsets to slice a UTF-8 string. Any stored text with multibyte characters (accents, CJK, emoji) will cause a panic at this point. A char-aware approach (chars().take(120).collect()) should be used instead. (This pattern pre-exists in the codebase at several other sites and should ideally be fixed globally.)--jsonis active, the command exits with code2(NOT_FOUND). Without--json, it exits0with a{"found": false}JSON body and no stderr message — misleading for non-JSON callers of a destructive command.wagl forget <id> --json, but--jsonis a global flag; the correct invocation iswagl --json forget <id>.Confidence Score: 2/5
Important Files Changed
Forgetsubcommand and handler; has three notable issues: (1)db.delete()return value ignored — can falsely report"deleted": trueon concurrent deletion, (2) byte-offset slicing on UTF-8 text can panic for non-ASCII content, (3) not-found path exits with code 0 in non-JSON mode vs. code 2 in JSON mode.Sequence Diagram
sequenceDiagram actor User participant CLI as wagl CLI participant DB as MemoryDb User->>CLI: wagl forget <id> [--confirm] CLI->>DB: connect_auto() + init() CLI->>DB: get(id) DB-->>CLI: Option<MemoryItem> alt Item not found CLI-->>User: json_mode=true → exit 2 NOT_FOUND\njson_mode=false → {"found":false} exit 0 else Item found — dry-run (no --confirm) CLI-->>User: {"mode":"dry-run","item":preview,"hint":"..."} else Item found — --confirm CLI->>DB: delete(id) [return value discarded ⚠️] DB->>DB: DELETE embeddings_meta (silent) DB->>DB: DELETE memory_edges (silent) DB->>DB: DELETE memory_item_vec (silent) DB->>DB: DELETE memory_items DB-->>CLI: u64 affected rows (ignored) CLI-->>User: {"deleted":true,"item":preview} endLast reviewed commit: 2165a0e
Context used: