From 5c59b598c6a172d6ea1f34f8bf7100aa1df3945c Mon Sep 17 00:00:00 2001 From: limiteinductive Date: Sun, 29 Mar 2026 22:51:48 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20codex=20review=20=E2=80=94=20scope=20ack?= =?UTF-8?q?=20to=20user,=20deduplicate=20inbox=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ack_inbox now requires ?user= and only acks items owned by that user - Reports actual rows_affected instead of input count - populate_inbox uses HashSet to prevent duplicate entries - @alice @alice no longer creates two inbox items - @alice @agents for an agent named alice only creates one item Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/colony-cli/src/client.rs | 2 +- crates/colony/src/routes.rs | 44 ++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/colony-cli/src/client.rs b/crates/colony-cli/src/client.rs index ab64800..5eaf173 100644 --- a/crates/colony-cli/src/client.rs +++ b/crates/colony-cli/src/client.rs @@ -90,7 +90,7 @@ impl ColonyClient { pub async fn ack_inbox(&self, ids: &[i64]) -> serde_json::Value { let body = AckRequest { ids: ids.to_vec() }; let res = self.http - .post(self.url("/api/inbox/ack")) + .post(self.url(&format!("/api/inbox/ack?{}", self.config.user_query()))) .json(&body) .send().await.unwrap_or_else(|e| { eprintln!("colony unreachable: {e}"); process::exit(1); }); if !res.status().is_success() { self.handle_error(res).await; } diff --git a/crates/colony/src/routes.rs b/crates/colony/src/routes.rs index 255cb77..422033d 100644 --- a/crates/colony/src/routes.rs +++ b/crates/colony/src/routes.rs @@ -392,31 +392,37 @@ pub async fn get_inbox( pub async fn ack_inbox( State(state): State, + Query(user_param): Query, Json(body): Json, ) -> Result { + let user_id = resolve_user(&state.db, &user_param).await?; + let mut acked = 0i64; for id in &body.ids { - sqlx::query("UPDATE inbox SET acked_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ? AND acked_at IS NULL") + let result = sqlx::query("UPDATE inbox SET acked_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ? AND user_id = ? AND acked_at IS NULL") .bind(id) + .bind(&user_id) .execute(&state.db) .await?; + acked += result.rows_affected() as i64; } - Ok(Json(serde_json::json!({"acked": body.ids.len()}))) + Ok(Json(serde_json::json!({"acked": acked}))) } /// Populate inbox entries when a message is posted async fn populate_inbox(db: &SqlitePool, message_id: &str, channel_id: &str, content: &str, sender_id: &str) { let mentions = crate::db::parse_mentions(content); + let mut notified: std::collections::HashSet = std::collections::HashSet::new(); for mention in &mentions { // Resolve mentioned user - if let Ok(Some(user_id)) = sqlx::query_scalar::<_, String>("SELECT id FROM users WHERE username = ?") + if let Ok(Some(uid)) = sqlx::query_scalar::<_, String>("SELECT id FROM users WHERE username = ?") .bind(mention) .fetch_optional(db) .await { - if user_id != sender_id { + if uid != sender_id && notified.insert(uid.clone()) { let _ = sqlx::query("INSERT INTO inbox (user_id, message_id, channel_id, trigger) VALUES (?, ?, ?, 'mention')") - .bind(&user_id) + .bind(&uid) .bind(message_id) .bind(channel_id) .execute(db) @@ -432,12 +438,14 @@ async fn populate_inbox(db: &SqlitePool, message_id: &str, channel_id: &str, con .await .unwrap_or_default(); for agent_id in agents { - let _ = sqlx::query("INSERT INTO inbox (user_id, message_id, channel_id, trigger) VALUES (?, ?, ?, 'broadcast')") - .bind(&agent_id) - .bind(message_id) - .bind(channel_id) - .execute(db) - .await; + if notified.insert(agent_id.clone()) { + let _ = sqlx::query("INSERT INTO inbox (user_id, message_id, channel_id, trigger) VALUES (?, ?, ?, 'broadcast')") + .bind(&agent_id) + .bind(message_id) + .bind(channel_id) + .execute(db) + .await; + } } } @@ -449,12 +457,14 @@ async fn populate_inbox(db: &SqlitePool, message_id: &str, channel_id: &str, con .await .unwrap_or_default(); for ape_id in apes { - let _ = sqlx::query("INSERT INTO inbox (user_id, message_id, channel_id, trigger) VALUES (?, ?, ?, 'broadcast')") - .bind(&ape_id) - .bind(message_id) - .bind(channel_id) - .execute(db) - .await; + if notified.insert(ape_id.clone()) { + let _ = sqlx::query("INSERT INTO inbox (user_id, message_id, channel_id, trigger) VALUES (?, ?, ?, 'broadcast')") + .bind(&ape_id) + .bind(message_id) + .bind(channel_id) + .execute(db) + .await; + } } } }