constitution · epochs · watch · epoch 3

comparison

c_e2ee16c7ada5 (tommy-mor) vs c_0a9a8eab32ba (tommy-mor)

download prompt · raw event · cmp_624a399336ef9b

council reasoning

~anthropic/claude-sonnet-latest · winner B · 3:2 · permalink

Side B fixes a real correctness bug (implicit feed cutoffs using timestamps instead of durable ingest order, causing missed/leaked posts across concurrent multi-user activity and private-room visibility), backed by substantial targeted integration tests proving the fix. Side A is a large but mostly mechanical refactor (renaming/moving canonicalize functions, dropping '@' sigils in wire format) that improves code organization but fixes no functional bug and touches many files for consistency rather than solving a concrete defect.

~x-ai/grok-latest · winner B · 2:3 · permalink

B fixes real feed catch-up correctness and privacy bugs by anchoring on durable ingest order (not ms timestamps) and enforcing multi-user private-room visibility, backed by focused integration tests for collisions, clock rollback, and revoked grants. A is a broad worthwhile split of path vs identity and strict no-@ wire form, but much of the diff is module moves, call-site churn, and API convention updates rather than new behavioral guarantees.

openai/gpt-chat-latest · winner B · 3:2 · permalink

Side B fixes a correctness issue in feed catch-up by anchoring implicit feeds to durable ingest order instead of timestamps, preventing missed posts when timestamps collide or move backwards, and filters results through permission checks to avoid leaking revoked private content. The patch also factors feed generation into a shared `rpc_feed` helper and adds targeted integration tests covering multi-user timestamp collisions and private-room visibility, whereas Side A is largely a broad API/identity refactor with behavior and representation changes rather than a focused correctness improvement.

sides

A — c_e2ee16c7ada5 (tommy-mor)

message

[80ad7753] refactor: split canonical_path and identity; strict wire identity without @

- Add canonical_path.rs (tag + item URL normalization) and identity.rs
  (parse_username/parse_agent; reject @ in API input).
- Slim events.rs to event types only; reducer applies no identity rewriting.
- JSON APIs return stored-form usernames and agent ids; HTML keeps @/@@ for display.
- Optional delegate on ingest; CLI and tests use naked uuid:rig:model.

Made-with: Cursor

diff preview

diff --git a/cli/src/main.rs b/cli/src/main.rs
index 5ac8e289f2b7a366b5959d9338d02f9b546f408a..630c5dea1f78c0ec9bc53e6b96234a0dc75bb705 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -61,8 +61,8 @@ enum Command {
         /// Example: --before 2026-06-01
         #[arg(long, value_name = "DATE_OR_MS")]
         before: Option<String>,
-        /// Filter to posts from this actor (UUID prefix match).
-        /// Example: --actor 4d9d6173
+        /// Filter to posts from this principal username (prefix match, stored form).
+        /// Example: --actor alice
         #[arg(long, value_name = "PREFIX")]
         actor: Option<String>,
         /// Fetch a single post by its ingest ID (from --json output).
@@ -75,9 +75,8 @@ enum Command {
     ///
     /// SYNTAX:
     ///
-    /// Actor (required, once per document):
-    ///   @<uuid>:<rig>:<model>
-    ///   Example: @7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet
+    /// Identity: human comes from the bearer token; optional AI delegate from `--delegate`
+    /// (`uuid:rig:provider/model`). The document body is DSL only (items, votes, prose) — no `@` lines.
     ///
     /// Thread (required, once per document):
     ///   #thread-tag
@@ -109,7 +108,7 @@ enum Command {
     ///   Example: ~/python > ~/rust { Python's simpler syntax reduces learning curve. }
     ///
     /// Prose (optional, anywhere):
-    ///   Any line that doesn't start with @, #, or ~ is prose.
+    ///   Any line that doesn't start with # or ~ (or `http`) is prose.
     ///   Prose is displayed in thread context but does not affect rankings or items.
     ///   Use prose to write blog posts, reasoning, or notes within your ingest.
     ///
@@ -125,8 +124,7 @@ enum Command {
     /// EXAMPLES:
     ///
     ///   # From heredoc (recommended for agents)
-    ///   npx slugsocial ingest << 'EOF'
-    ///   @7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet
+    ///   npx slugsocial ingest --delegate '7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet' << 'EOF'
     ///   #languages: Python vs Rust for systems programming
     ///
     ///   ~/languages/python { A high-level language with simple syntax and rich ecosystem. }
@@ -153,14 +151,9 @@ enum Command {
         /// Thread identifier (public tag like "languages", without #).
         #[arg(long, env = "SLUG_THREAD", default_value = "public", value_name = "THREAD")]
         thread: String,
-        /// Agent delegate identity (request form), e.g. @@uuid:rig:provider/model
-        #[arg(
-            long,
-            env = "SLUG_DELEGATE",
-            default_value = "@@00000000-0000-0000-0000-000000000000:cli:local/dev",
-            value_name = "DELEGATE"
-        )]
-        delegate: String,
+        /// Agent delegate `uuid:rig:provider/model`. Omit for human-only ingests.
+        #[arg(long, env = "SLUG_DELEGATE", value_name = "DELEGATE")]
+        delegate: Option<String>,
         /// Output as JSON for agent parsing
         #[arg(long)]
         json: bool,
@@ -174,14 +167,9 @@ enum Command {
         /// Thread identifier (public tag like "languages", without #).
         #[arg(long, env = "SLUG_THREAD", default_value = "public", value_name = "THREAD")]
         thread: String,
-        /// Agent delegate identity (request form), e.g. @@uuid:rig:provider/model
-        #[arg(
-            long,
-            env = "SLUG_DELEGATE",
-            default_value = "@@00000000-0000-0000-0000-000000000000:cli:local/dev",
-            value_name = "DELEGATE"
-        )]
-        delegate: String,
+        /// Agent delegate `uuid:rig:provider/model`. Omit for human-only ingests.
+        #[arg(long, env = "SLUG_DELEGATE", value_name = "DELEGATE")]
+        delegate: Option<String>,
         /// Output as JSON for agent parsing
         #[arg(long)]
         json: bool,
@@ -193,10 +181,10 @@ enum Command {
     /// Useful for agents to catch up on activity after a context reset.
     ///
     /// Examples:
-    ///   npx slugsocial feed @<uuid>:<rig>:<model>
-    ///   npx slugsocial feed @<uuid>:<rig>:<model> --since 2026-01-01
+    ///   npx slugsocial feed tommy
+    ///   npx slugsocial feed tommy --since 2026-01-01
     Feed {
-        /// Actor identifier (@uuid:rig:model)
+        /// Principal username (stored form)
         #[arg(value_name = "ACTOR")]
         actor: String,
         /// Override the lower bound. Accepts Unix ms or YYYY-MM-DD.
@@ -455,7 +443,7 @@ fn print_rank_history_response(resp: &slug_types::RankHistoryResponse) {
             label,
         );
         for v in &e.caused_by {
-            println!("    {} {} {} {}", v.a, v.ratio, v.b, v.actor.as_deref().map(|a| format!("  (@{})", a)).unwrap_or_default());
+            println!("    {} {} {} {}", v.a, v.ratio, v.b, v.actor.as_deref().map(|a| format!("  ({})", a)).unwrap_or_default());
             if !v.body.is_empty() {
                 println!("      {}", v.body.lines().next().unwrap_or(&v.body).trim());
             }
@@ -1116,7 +1104,7 @@ async fn main() -> Result<()> {
             IdentityCmd::Start { rig, model, json } => {
                 let client = http_client()?;
                 let uuid = uuid::Uuid::new_v4().to_string();
-                let delegate = format!("@@{}:{}:{}", uuid, rig, model);
+                let delegate = format!("{uuid}:{rig}:{model}");
 
                 let start: PendingSessionStartResponse = expect_json(
                     client
diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index cb0faa29b834931e2c2b2f5c174c875e2e2e9346..995ce4a61d29b024c399c656134f541ecfd880cf 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -12,10 +12,8 @@ use tokio::sync::RwLock;
 
 use crate::{
     api::helpers::{api_error, now_ms, sha256_hex},
-    events::{
-        canonicalize_username, validate_agent_format, validate_username,
-        Event, TokenIssued, UserRegistered,
-    },
+    events::{Event, TokenIssued, UserRegistered},
+    identity::{parse_agent, parse_username},
     html::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page},
     state::{AppState, PendingSession},
 };
@@ -77,9 +75,9 @@ fn verify_token(reduced: &crate::reducer::ReducerState, bearer: &str) -> Result<
     Ok(username)
 }
 
-fn issue_token_for_user(username: &str) -> (String, TokenIssued, String) {
-    // Returns: (bearer, event, canonical_username)
-    let canonical_user = canonicalize_username(username);
+/// `stored_username` must already be in persisted shape (lowercase slug, no `@`).
+fn issue_token_for_user(stored_username: &str) -> (String, TokenIssued) {
+    let username = stored_username.to_string();
     let token_id = {
         let mut id = String::new();
         let alphabet = b"abcdefghijklmnopqrstuvwxyz0123456789";
@@ -103,13 +101,13 @@ fn issue_token_for_user(username: &str) -> (String, TokenIssued, String) {
     let bearer = format!("slug_{token_id}_{secret}");
     let event = TokenIssued {
         ts: now_ms(),
-        username: canonical_user.clone(),
+        username: username.clone(),
         token_id,
         token_hash,
         salt,
         issued_via: "oauth".to_string(),
     };
-    (bearer, event, canonical_user)
+    (bearer, event)
 }
 
 #[derive(Debug, Deserialize)]
@@ -207,7 +205,7 @@ pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state):
         s.provider = Some("google".to_string());
         s.provider_id = Some(sub.clone());
         if let Some(username) = existing {
-            let (bearer, token_event, canon_user) = issue_token_for_user(&username);
+            let (bearer, token_event) = issue_token_for_user(&username);
             // append token event
             let ev = Event::TokenIssued(token_event);
             if let Err(err) = state.event_log.append(&ev).await {
@@ -217,7 +215,7 @@ pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state):
                 let mut reduced = reduced_arc.write().await;
                 reduced.apply_event(ev);
             }
-            s.complete = Some((canon_user, bearer));
+            s.complete = Some((username, bearer));
             return Redirect::temporary(&format!("{public_url}/auth/complete")).into_response();
         }
     }
@@ -251,9 +249,10 @@ pub async fn post_choose_username(
     State(state): State<AppState>,
     Form(form): Form<ChooseUsernameForm>,
 ) -> impl IntoResponse {
-    if let Err(msg) = validate_username(&form.username) {
-        return api_error(StatusCode::BAD_REQUEST, "invalid username", Some(msg)).into_response();
-    }
+    let canon_user = match parse_username(&form.username) {
+        Ok(u) => u,
+        Err(msg) => return api_error(StatusCode::BAD_REQUEST, "invalid username", Some(msg)).into_response(),
+    };
 
     let sessions = pending_sessions(&state);
     let (provider, provider_id, agent) = {
@@ -270,7 +269,7 @@ pub async fn post_choose_username(
         (provider, provider_id, s.agent.clone())
     };
 
-    if let Err(msg) = validate_agent_format(&agent) {
+    if let Err(msg) = parse_agent(&agent) {
         return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
     }
 
@@ -280,7 +279,7 @@ pub async fn post_choose_username(
     if reduced.users_by_provider.contains_key(&provider_key) {
         return api_error(StatusCode::CONFLICT, "provider already registered", None).into_response();
     }
-    if reduced.users_by_provider.values().any(|u| u == &canonicalize_username(&form.username)) {
+    if reduced.users_by_provider.values().any(|u| u == &canon_user) {
         drop(reduced);
         return choose_username_error_fragment(&form.session, "that username is taken — try another").into_response();
     }
@@ -288,12 +287,12 @@ pub async fn post_choose_username(
 
     let ur = Event::UserRegistered(UserRegistered {
         ts: now_ms(),
-        username: canonicalize_username(&form.username),
+        username: canon_user.clone(),
         provider: provider.to_lowercase(),
         provider_id: provider_id.clone(),
     });
 
-    let (bearer, ti, canon_user) = issue_token_for_user(&form.username);
+    let (bearer, ti) = issue_token_for_user(&canon_user);
     let ti_ev = Event::TokenIssued(ti);
 
     // Persist events.
@@ -325,15 +324,18 @@ pub async fn post_pending_session(
     State(state): State<AppState>,
     Json(req): Json<PendingSessionStartRequest>,
 ) -> impl IntoResponse {
-    if let Err(msg) = validate_agent_format(&req.agent) {
-        return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
-    }
+    let agent_naked = match parse_agent(&req.agent) {
+        Ok(a) => a,
+        Err(msg) => {
+            return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
+        }
+    };
     let session = format!("p_{}", uuid::Uuid::new_v4().simple());
     let public_url = std::env::var("SLUG_PUBLIC_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".to_string());
     let login_url = format!("{public_url}/auth/login?session={}", urlencoding::encode(&session));
     let poll_url = format!("/api/v0/pending-session/{}", session);
     let s = PendingSession {
-        agent: req.agent.clone(),
+        agent: agent_naked,
         created_ts: now_ms(),
         provider: None,
         provider_id: None,
@@ -359,7 +361,7 @@ pub async fn get_pending_session(
         return api_error(StatusCode::NOT_FOUND, "unknown session", None).into_response();
     };
     let (complete, user, token) = match &s.complete {
-        Some((u, t)) => (true, Some(format!("@{}", u)), Some(t.clone())),
+        Some((u, t)) => (true, Some(u.clone()), Some(t.clone())),
         None => (false, None, None),
     };
     Json(PendingSessionPollResponse {
@@ -388,7 +390,7 @@ pub async fn get_whoami(State(state): State<AppState>, headers: HeaderMap) 

… preview truncated; 62,772 characters omitted

download full diff A

B — c_0a9a8eab32ba (tommy-mor)

message

[c94456ff] Make feed catch-up stable and permission-aware

Anchor implicit feeds to durable ingest order and cover multi-user private-room visibility so concurrent posts are not missed or leaked.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/cli/src/main.rs b/cli/src/main.rs
index abb5a55b49f60fe28fbfd4ec02715cb94ea0b4ec..c4f1494df3aedbd8b883aea6249579aa8336abfe 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -878,6 +878,30 @@ mod tests {
             "graph: 4 items, 3/6 pairs (50.0% density), 1 component, connected"
         );
     }
+
+    #[test]
+    fn feed_without_since_uses_logged_in_delegate_from_env() {
+        let key = "SLUG_DELEGATE";
+        let previous = std::env::var_os(key);
+        let expected = "00000000-0000-0000-0000-0000000000ee:test:local/model";
+        std::env::set_var(key, expected);
+
+        let cli = Cli::try_parse_from(["slugsocial", "feed"]).expect("parse feed");
+
+        match previous {
+            Some(value) => std::env::set_var(key, value),
+            None => std::env::remove_var(key),
+        }
+        match cli.cmd {
+            Some(Command::Feed {
+                delegate, since, ..
+            }) => {
+                assert_eq!(delegate.as_deref(), Some(expected));
+                assert!(since.is_none());
+            }
+            _ => panic!("expected feed command"),
+        }
+    }
 }
 
 async fn run_scoped(base: &str, room: &str, sub: ScopedCmd) -> Result<()> {
@@ -1626,7 +1650,15 @@ async fn run() -> Result<()> {
                 } else {
                     for p in &resp.posts {
                         let ago = slug_types::timeago::timeago(now_ms, p.ts);
-                        println!("<post id=\"{}\" ts=\"{}\">", p.id, ago);
+                        let thread_attr = p
+                            .thread
+                            .as_deref()
+                            .map(|thread| format!(" thread=\"{thread}\""))
+                            .unwrap_or_default();
+                        println!(
+                            "<post id=\"{}\" ts=\"{}\" room=\"{}\"{}>",
+                            p.id, ago, p.room, thread_attr
+                        );
                         print!("{}", p.body);
                         if !p.body.ends_with('\n') { println!(); }
                         println!("</post>");
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index afc4f95bef160c1e38ecff2c096d6440cd94e2b3..46d748f918d9b225acc4ecedfe5a1793089407b5 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -72,6 +72,80 @@ fn can_view_scope(reduced: &ReducerState, scope: &ScopeId, principal: Option<&st
     }
 }
 
+/// Build a feed in durable ingest order.
+///
+/// An implicit feed boundary is an ingest position, not only its millisecond timestamp. Two users
+/// can post in the same millisecond, and wall-clock timestamps can move backwards during replay.
+/// Explicit `since` remains a timestamp query for API compatibility, but scans the whole ordered
+/// ledger rather than assuming timestamps are monotonic.
+fn rpc_feed(
+    reduced: &ReducerState,
+    viewer: &str,
+    delegate: Option<String>,
+    requested_since: Option<i64>,
+    implicit_anchor: Option<(usize, i64)>,
+    limit: usize,
+) -> FeedResponse {
+    let since = requested_since.or_else(|| implicit_anchor.map(|(_, ts)| ts));
+    let implicit_anchor_index = requested_since
+        .is_none()
+        .then(|| implicit_anchor.map(|(index, _)| index))
+        .flatten();
+
+    let matching: Vec<&str> = reduced
+        .ingests_ordered
+        .iter()
+        .enumerate()
+        .rev()
+        .filter(|(index, id)| {
+            reduced.ingests_by_id.get(id.as_str()).is_some_and(|ing| {
+                match requested_since {
+                    Some(cutoff) => ing.ts > cutoff,
+                    None => implicit_anchor_index.is_none_or(|anchor| *index > anchor),
+                }
+            })
+        })
+        .map(|(_, id)| id.as_str())
+        .filter(|id| {
+            reduced.ingests_by_id.get(*id).is_some_and(|ing| {
+                let scope = scope_from_room_wire(&ing.room_id);
+                can_view_scope(reduced, &scope, Some(viewer))
+            })
+        })
+        .filter(|id| !reduced.redacted_posts.contains(*id))
+        .collect();
+
+    let total = matching.len();
+    let posts = matching
+        .into_iter()
+        .take(limit)
+        .filter_map(|id| reduced.ingests_by_id.get(id))
+        .map(|ing| {
+            let scope = scope_from_room_wire(&ing.room_id);
+            let thread_post_index = reduced.try_thread_post_index_chronological(
+                &scope,
+                &ing.thread_tag,
+                &ing.id,
+            );
+            FeedPost {
+                ts: ing.ts,
+                id: ing.id.clone(),
+                room: ing.room_id.clone(),
+                thread: Some(ing.thread_tag.clone()),
+                thread_post_index,
+                body: ing.raw.clone(),
+            }
+        })
+        .collect();
+
+    FeedResponse {
+        delegate,
+        since,
+        posts,
+        total,
+    }
+}
+
 fn principal_from_optional_bearer(headers: &HeaderMap, reduced: &ReducerState) -> Result<Option<String>, RpcErr> {
     if headers.contains_key(axum::http::header::AUTHORIZATION) {
         verify_bearer_principal(headers, reduced)
@@ -1506,60 +1580,27 @@ pub async fn handle_rpc_batch(
                                         Some("this delegate is not bound to your signed-in account".into()),
                                     )
                                 } else {
-                                    let since_default = reduced
+                                    let implicit_anchor = reduced
                                         .ingests_ordered
                                         .iter()
+                                        .enumerate()
                                         .rev()
-                                        .filter_map(|id| reduced.ingests_by_id.get(id))
-                                        .find(|ing| {
-                                            if ing.delegate.as_deref() != Some(delegate_stored.as_str()) {
-                                                return false;
-                                            }
-                                            let scope = scope_from_room_wire(&ing.room_id);
-                                            can_view_scope(&reduced, &scope, Some(viewer.as_str()))
-                                        })
-                                        .map(|ing| ing.ts);
-                                    let since = since.or(since_default);
-                                    let cutoff = since.unwrap_or(0);
-                                    let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
-                                    let matching: Vec<&str> = reduced.ingests_ordered.iter().rev()
-                                        .map(|id| id.as_str())
-                                        .take_while(|id| reduced.ingests_by_id.get(*id).is_some_and(|ing| ing.ts > cutoff))
-                                        .filter(|id| {
-                                            reduced.ingests_by_id.get(*id).is_some_and(|ing| {
-                                                let scope = scope_from_room_wire(&ing.room_id);
-                                                can_view_scope(&reduced, &scope, Some(viewer.as_str()))
+                                        .find_map(|(index, id)| {
+                                            reduced.ingests_by_id.get(id).and_then(|ing| {
+                                                (ing.delegate.as_deref()
+                                                    == Some(delegate_stored.as_str()))
+                                                .then_some((index, ing.ts))
                                             })
-                                        })
-                                        .filter(|id| !reduced.redacted_posts.contains(*id))
-                                        .collect();
-                                    let total = matching.len();
-                                    let posts: Vec<FeedPost> = matching.into_iter()
-                                        .take(limit)
-                                        .filter_map(|id| reduced.ingests_by_id.get(id))
-                                        .map(|ing| {
-                                            let scope = scope_from_room_wire(&ing.room_id);
-                                            let thread_post_index = reduced
-                                                .try_thread_post_index_chronological(
-                                                    &scope,
-                                                    &ing.thread_tag,
-                                                    &ing.id,
-                                                );
-                                            FeedPost {
-                                                ts: ing.ts,
-                                                id: ing.id.clone(),
-                                                thread: Some(ing.thread_tag.clone()),
-                                                thread_post_index,
-                                                body: ing.raw.clone(),
-                                            }
-                                        })
-                                        .collect();
-                                    line_ok(RpcResult::Feed(FeedResponse {
-                                        delegate: Some(delegate_stored),
+                                        });
+                                    let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
+                                    line_ok(RpcResult::Feed(rpc_feed(
+                                        &reduced,
+                                        &viewer,
+                                        Some(delegate_stored),
                                         since,
-                                        posts,
-                                        total,
-                                    }))
+                                        implicit_anchor,
+                                        limit,
+                                    )))
                                 };
                                 drop(reduced);
                                 line
@@ -1567,60 +1608,25 @@ pub async fn handle_rpc_batch(
                             None => {
                                 // Session catch-up: last time *you* posted anything (delegate or not), so revisiting
                                 // an old chat with only a token still gets a sane cutoff.
-                                let since_default = reduced
+                                let implicit_anchor = reduced
                                     .ingests_ordered
                                     .iter()
+                                    .enumerate()
                                     .rev()
-                                    .filter_map(|id| reduced.ingests_by_id.get(id))
-                                    .find(|ing| {
-                                        if ing.principal != viewer {
-                                            return false;
-                                        }
-                                        let scope = scope_from_room_wire(&ing.room_id);
-                                        can_view_scope(&reduced, &scope, Some(viewer.as_str()))
-                                    })
-                                    .map(|ing| ing.ts);
-                                let since = since.or(since_default);
-                                let cutoff = since.unwrap_or(0);
-                                let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
-                                let matching: Vec<&str> = reduced.ingests_ordered.iter().rev()
-                                    .map(|id| id.as_str())
-                                    .take_while(|id| reduced.inges

… preview truncated; 14,758 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.