constitution · epochs · watch · epoch 3

comparison

c_25172cf8caa0 (tommy-mor) vs c_e4c160a5feb7 (tommy-mor)

download prompt · raw event · cmp_2daf87f61bee8e

council reasoning

~anthropic/claude-sonnet-latest · winner A · 85:15 · permalink

Side A fixes a real concurrency bug (shared browser sentinel delegate blocking multi-user votes) with a coherent design change across server, CLI, types, and tests, including a new integration test proving the fix. Side B is a minor cosmetic HTML cleanup removing headers/labels with no functional change or test coverage, offering far less lasting value.

~x-ai/grok-latest · winner A · 12:1 · permalink

A fixes a real multi-user correctness bug by removing shared browser sentinel delegates (PendingSession.agent becomes Option, votes/posts use no delegate, authorship simplified) with integration coverage; B only deletes cosmetic forum chrome (compose heading/help text and thread subtitle/garden link).

openai/gpt-chat-latest · winner A · 20:1 · permalink

Side A fixes a substantive correctness issue by removing shared browser sentinel delegates, changing pending sessions to use an optional delegate for browser logins, updating attribution logic, and adding integration tests that verify multiple human users can vote without erroneous shared AgentBound conflicts. Side B only makes minor UI cleanup by removing or relocating headings and help text in the compose view, with no evident behavioral improvement.

sides

A — c_25172cf8caa0 (tommy-mor)

message

[b9476669] Remove browser sentinel delegates so multi-user votes work.

Shared WEB_BROWSER_AGENT bound on first vote and blocked every later human; browser posts now use no delegate, matching forum UI.

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

diff preview

diff --git a/cli/src/main.rs b/cli/src/main.rs
index c4f1494df3aedbd8b883aea6249579aa8336abfe..af3e4fee876910a44f6f872b24821bc541a23e20 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -1741,8 +1741,8 @@ async fn run() -> Result<()> {
                     tokio::time::sleep(std::time::Duration::from_millis(poll_interval_ms)).await;
                     let poll: PendingSessionPollResponse =
                         expect_json(client.get(&poll_url).send().await?).await?;
-                    if !poll.agent.trim().is_empty() {
-                        agent_out = Some(poll.agent.clone());
+                    if let Some(a) = poll.agent.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
+                        agent_out = Some(a.to_string());
                     }
                     if poll.complete {
                         token_out = poll.token;
diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index 50dc6af908412b16343829eae25154ebf4e19fca..01c02f50c19bcd62c7f1717f9b927f203c3164d0 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -29,18 +29,6 @@ use crate::{
     write_cmd::WriteCmd,
 };
 
-/// Delegate id for browser users who land via `/join/inv_…` (no CLI agent).
-const INVITE_BROWSER_AGENT: &str = "00000000-0000-0000-0000-000000000000:invite:web/join";
-
-/// Agent id for `/login` browser OAuth (no CLI); must pass [`parse_agent`].
-pub const WEB_BROWSER_AGENT: &str = "00000000-0000-0000-0000-000000000001:social:web/browser";
-
-/// True for well-known browser / human-form sentinel delegates (not real AI agents).
-/// HTML attribution should show the human username for these, not `@@uuid:rig:…`.
-pub fn is_browser_sentinel_delegate(agent: &str) -> bool {
-    agent == WEB_BROWSER_AGENT || agent == INVITE_BROWSER_AGENT
-}
-
 /// HttpOnly cookie storing the same `slug_*` bearer string the CLI uses.
 pub const SLUG_SESSION_COOKIE: &str = "slug_session";
 
@@ -288,7 +276,7 @@ pub async fn get_join_invite(
     let session = format!("p_{}", uuid::Uuid::new_v4().simple());
     let redirect_next = safe_local_redirect(q.next.as_deref().or(q.redirect.as_deref()));
     let s = PendingSession {
-        agent: INVITE_BROWSER_AGENT.to_string(),
+        agent: None,
         created_ts: now_ms(),
         provider: None,
         provider_id: None,
@@ -555,7 +543,7 @@ pub async fn post_choose_username(
     };
 
     let sessions = pending_sessions(&state);
-    let (provider, provider_id, agent) = {
+    let (provider, provider_id) = {
         let sessions_read = sessions.read().await;
         let Some(s) = sessions_read.get(&form.session) else {
             return api_error(StatusCode::NOT_FOUND, "unknown session", None).into_response();
@@ -566,14 +554,9 @@ pub async fn post_choose_username(
         let Some(provider_id) = s.provider_id.clone() else {
             return js_form_error_fragment(&form.session, "oauth not completed").into_response();
         };
-        (provider, provider_id, s.agent.clone())
+        (provider, provider_id)
     };
 
-    if let Err(msg) = parse_agent(&agent) {
-        return js_form_error_fragment(&form.session, &format!("invalid agent format — {msg}"))
-            .into_response();
-    }
-
     let redeem_invite = {
         let sessions_read = sessions.read().await;
         sessions_read
@@ -643,7 +626,8 @@ pub async fn get_web_login(
     let redirect_next = safe_local_redirect(q.next.as_deref().or(q.redirect.as_deref()))
         .or_else(|| Some("/".to_string()));
     let s = PendingSession {
-        agent: WEB_BROWSER_AGENT.to_string(),
+        // Humans sign in via the website with no AI delegate.
+        agent: None,
         created_ts: now_ms(),
         provider: None,
         provider_id: None,
@@ -704,7 +688,7 @@ pub async fn post_pending_session(
     );
     let poll_url = format!("/api/v0/pending-session/{session}");
     let s = PendingSession {
-        agent: agent_naked,
+        agent: Some(agent_naked),
         created_ts: now_ms(),
         provider: None,
         provider_id: None,
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index fdff6db0472b36cd7870a4be68574023e0daf120..920e967b47852ea82fa61b84c457ae3582dd9800 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -18,13 +18,11 @@ pub use auth::{
     get_choose_username,
     get_web_login,
     get_logout,
-    is_browser_sentinel_delegate,
     optional_principal,
     resolve_web_session,
     session_cookie_header_value,
     WebSession,
     SLUG_SESSION_COOKIE,
-    WEB_BROWSER_AGENT,
 };
 
 pub use helpers::{
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 611956d0a46062b49ce350be176e4be139312ff0..6e56c039a184953cd1d0593c34cf4d5abe41f2a3 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -277,7 +277,7 @@ async fn dispatch_ui_action(
                 &session.bearer,
                 room.clone(),
                 thread_tag.clone(),
-                Some(crate::api::auth::WEB_BROWSER_AGENT.to_string()),
+                None,
                 text,
             )
             .await
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 645d54f2e117ba901e02ed958ada6ff69a78ae34..03ad762e88709c77b9d5dd130c48bc96226616a1 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -560,27 +560,28 @@ fn identity_color_css(seed: &str) -> String {
     format!("hsl({hue}, 62%, 66%)")
 }
 
-/// Seed for author color: real AI → delegate uuid; human/sentinel → principal username.
+/// Seed for author color: AI → delegate uuid; human (no delegate) → principal username.
 fn authorship_color_seed<'a>(principal: &'a str, delegate: &'a Option<String>) -> &'a str {
     match delegate {
-        Some(d) if !crate::api::is_browser_sentinel_delegate(d) => {
-            d.split(':').next().filter(|s| !s.is_empty()).unwrap_or(d.as_str())
-        }
-        _ => principal,
+        Some(d) => d
+            .split(':')
+            .next()
+            .filter(|s| !s.is_empty())
+            .unwrap_or(d.as_str()),
+        None => principal,
     }
 }
 
-/// Prefer the AI delegate in attribution; fall back to the human username when there is no
-/// delegate or the delegate is a browser/human-form sentinel.
+/// Prefer the AI delegate in attribution; humans post with no delegate and show `@username`.
 pub(crate) fn authorship_attr(principal: &str, delegate: &Option<String>) -> AuthorshipAttr {
     let color = identity_color_css(authorship_color_seed(principal, delegate));
     match delegate {
-        Some(d) if !crate::api::is_browser_sentinel_delegate(d) => AuthorshipAttr {
+        Some(d) => AuthorshipAttr {
             label: format!("@@{}", actor_label(d)),
             author_title: Some(format!("@{principal}")),
             color,
         },
-        _ => AuthorshipAttr {
+        None => AuthorshipAttr {
             label: format!("@{principal}"),
             author_title: None,
             color,
@@ -933,7 +934,6 @@ pub(super) fn recency_class(now_ms: i64, ts_ms: i64) -> &'static str {
 #[cfg(test)]
 mod authorship_tests {
     use super::*;
-    use crate::api::WEB_BROWSER_AGENT;
 
     #[test]
     fn human_or_missing_delegate_shows_username() {
@@ -943,15 +943,6 @@ mod authorship_tests {
         assert!(a.color.starts_with("hsl("));
     }
 
-    #[test]
-    fn browser_sentinel_delegate_shows_username() {
-        let d = Some(WEB_BROWSER_AGENT.to_string());
-        let a = authorship_attr("alice", &d);
-        assert_eq!(a.label, "@alice");
-        assert_eq!(a.author_title, None);
-        assert_eq!(authorship_address("alice", &d), "@alice");
-    }
-
     #[test]
     fn real_ai_delegate_shows_short_agent_with_username_hover() {
         let d = Some(
diff --git a/server/src/state.rs b/server/src/state.rs
index 648ab5304764a329fcabbbbcd3782b94e3e005a8..8ea84dcf9b1df8f8037e913cdd94e5908e6d5d55 100644
--- a/server/src/state.rs
+++ b/server/src/state.rs
@@ -21,7 +21,8 @@ pub struct InviteState {
 
 #[derive(Debug, Clone)]
 pub struct PendingSession {
-    pub agent: String,
+    /// CLI `identity start` delegate (`uuid:rig:model`). `None` for browser `/login` and `/join`.
+    pub agent: Option<String>,
     pub created_ts: i64,
     pub provider: Option<String>,
     pub provider_id: Option<String>,
diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs
index 6b1475b773106a2dd3f326475c9fb4cc727f6b4b..714563a8b330e3917d95a54ae29b4de143e3b8a4 100644
--- a/server/tests/integration_ui.rs
+++ b/server/tests/integration_ui.rs
@@ -418,6 +418,99 @@ async fn test_web_login_carries_vote_pair_next_into_pending_session() {
     let sessions = state.pending_sessions.read().await;
     let pending = sessions.get(&session).expect("pending session");
     assert_eq!(pending.redirect_next.as_deref(), Some(next));
+    assert_eq!(
+        pending.agent, None,
+        "browser /login must not invent a sentinel delegate"
+    );
+}
+
+#[tokio::test]
+async fn test_vote_compare_two_users_both_succeed_without_delegate() {
+    let (addr, _tmp, _log, state, _handle) = create_test_server_with_state().await;
+    let client = reqwest::Client::new();
+    let alice = test_bearer();
+    let bob = seed_test_identity(&state, "bob", "bobtok", "bobsecret").await;
+
+    // Define items first (votes require existing item bodies).
+    let seed = ui_post_ingest_rpc(
+        "public",
+        "multi-vote",
+        "~/multi-a {alpha}\n~/multi-b {beta}\n",
+    );
+    let seed_resp = client
+        .post(format!("http://{addr}/ui"))
+        .header("Authorization", format!("Bearer {alice}"))
+        .form(&[("__rpc__", seed.as_str())])
+        .send()
+        .await
+        .unwrap();
+    assert_eq!(seed_resp.status(), reqwest::StatusCode::OK);
+    let seed_js = seed_resp.text().await.unwrap();
+    assert!(
+        !seed_js.contains("auth-error"),
+        "item seed must succeed, got: {seed_js}"
+    );
+
+    for (bearer, left, right, explanation) in [
+        (&alice, "3", "1", "alice prefers a"),
+        (&bob, "1", "3", "bob prefers b"),
+    ] {
+        let rpc = ui_vote_compare_post_rpc(
+            "public",
+            "multi-vote",
+            "~/multi-a",
+            "~/multi-b",
+            left,
+            right,
+            explanation,
+        );
+        let resp = client
+            .post(format!("http://{addr}/ui"))
+            .header("Authorization", format!("Bearer {bearer}"))
+            .form(&[("__rpc__", rpc.as_str())])
+            .send()
+            .await
+            .unwrap();
+        assert_eq!(resp.status(), reqwest::StatusCode::OK);
+        let js = resp.text().await.unwrap();
+        assert!(
+            !js.contains("delegate already bound"),
+            "human vote must not hit shared-sentinel AgentBound ({explanation}), got: {js}"
+        );
+        assert!(
+            !js.contains("auth-error"),
+            "human vote must succeed ({explanation}), got: {js}"
+        );
+        assert!(
+            js.contains("vote-edge-history-region"),
+            "vote should morph edge history ({explanation}), got: {js}"
+        );
+    }
+
+    let reduced = state.reduced.read().await;
+    let human_votes: Vec<_> = reduced
+        .ingests_ordered
+        .iter()
+        .filter_map(|id| reduced.ingests_by_id.get(id))
+        .filter(|ing| ing.raw.contains("prefers"))
+        .collect();
+    assert_eq!(human_votes.len(), 2, "expected two vote ingests");
+    let mut principals: Vec<&str> = human_votes.iter().map(|i| i.principal.as_str()).collect();
+    principals.sort();
+    assert_eq!(principals, ["bob", "testuser"]);
+    for ing in &human_votes {
+        assert!(
+            ing.delegate.is_none(),
+            "browser votes must have no delegate, principal={} delegate={:?}",
+            ing.principal,
+            ing.delegate
+        );
+    }
+    assert!(
+        reduced.agent_bindings.is_empty(),
+        "human

… preview truncated; 3,004 characters omitted

download full diff A

B — c_e4c160a5feb7 (tommy-mor)

message

[ba5a8b00] view cleaner

diff preview

diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
index 493b7596a1d334b174ee9916571446d14507ec97..ab7970feca47a4431ddc19dab2c8186180b78413 100644
--- a/server/src/html/forum.rs
+++ b/server/src/html/forum.rs
@@ -426,9 +426,6 @@ fn compose_form(nav: &ThreadNav, thread_tag: &str, show: bool) -> Markup {
     }
     html! {
         section class="compose" id="thread-compose" {
-            h3 { "reply" }
-            p class="muted" { "Uses the same ingest DSL as the CLI. You must be logged in." }
-            div id="thread-compose-errors" {}
             form id="thread-compose-form" method="POST" action="/post" data-check-action="/post/check" {
                 input type="hidden" name="room" value=(nav.room_wire.clone());
                 input type="hidden" name="thread_tag" value=(thread_tag);
@@ -439,6 +436,7 @@ fn compose_form(nav: &ThreadNav, thread_tag: &str, show: bool) -> Markup {
                     button type="submit" { "post" }
                 }
             }
+            div id="thread-compose-errors" {}
         }
     }
 }
@@ -756,14 +754,7 @@ async fn thread_view_inner(
         html! {
             (strip)
             nav class="breadcrumb" { (bc) }
-            h2 { "#" (tag) @if let Some(sub) = &subtitle { ": " (sub) } }
             p class="muted" { "top=oldest · bottom=newest" }
-            @if matches!(sc, ScopeId::Room(_)) {
-                p class="muted" {
-                    "room garden · "
-                    a href=(nav.garden_root_url()) { "~" }
-                }
-            }
             div id="thread-feed-region" {
                 @if display_ingests.is_empty() {
                     p class="muted" { "no activity yet" }

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.