constitution · epochs · watch · epoch 3

comparison

c_2dc96aace098 (tommy-mor) vs c_e2ee16c7ada5 (tommy-mor)

download prompt · raw event · cmp_3605aecadb092b

council reasoning

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

Side A vendors a whole speculative RocksDB abstraction crate (with docs, examples, benchmarks, licenses) plus a huge dependency/Cargo.lock churn, but the actual integration into the server is thin (one entity_store wrapper) and the streaming replay refactor is modest; much of the diff is generated noise rather than durable design. Side B is a focused, coherent refactor that removes ad-hoc identity canonicalization scattered across events/reducer, introduces clear identity.rs/canonical_path.rs modules, fixes wire-format inconsistencies (@ handling), and updates tests/CLI/docs consistently, yielding a cleaner, more correct codebase with less risk and less bloat.

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

Commit A delivers lasting architectural value by introducing a full RocksDB-backed durable crate, offloading entity JSON payloads out of the in-memory GlobalTree into EntityStore, and replacing bulk event load with true line-at-a-time replay to cut startup RAM. Commit B is a worthwhile but narrower refactor that extracts canonical_path/identity modules and enforces naked stored-form identities on the wire; it improves modularity and API consistency without adding comparable runtime capability or resource wins.

openai/gpt-chat-latest · winner A · 9:4 · permalink

Side A introduces substantial new infrastructure: a new RocksDB-backed `durable` workspace crate, an `EntityStore` that moves raw Reddit JSON out of in-memory state, and streaming event-log replay via `EventLog::replay`, reducing startup memory while preserving functionality. Side B is a worthwhile architectural refactor that separates path normalization and identity parsing, enforces stored-form identities without `@`, and updates APIs accordingly, but it primarily reorganizes validation and representation rather than adding comparable runtime capabilities.

sides

A — c_2dc96aace098 (tommy-mor)

message

[285b64d4] Offload Reddit payloads to RocksDB and stream event log replay.

Vendor durable as a workspace crate, store entity JSON in entity_db instead
of GlobalTree, and replay events.jsonl one line at a time to cut startup RAM.

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

diff preview

diff --git a/Cargo.lock b/Cargo.lock
index e55d87f32ab32064686431c7082ef8c9ca872d63..8fc09f9ac978bd7ccf57f177989057b606677db8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,18 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
 [[package]]
 name = "anyhow"
 version = "1.0.102"
@@ -56,6 +68,12 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
 [[package]]
 name = "axum"
 version = "0.7.9"
@@ -153,6 +171,75 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.65.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags 2.11.1",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.2",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "bitflags"
 version = "2.11.1"
@@ -171,6 +258,22 @@ version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
 
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
 [[package]]
 name = "cc"
 version = "1.2.62"
@@ -178,15 +281,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
 dependencies = [
  "find-msvc-tools",
+ "jobserver",
+ "libc",
  "shlex",
 ]
 
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
 
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
 [[package]]
 name = "cookie"
 version = "0.18.1"
@@ -224,6 +401,73 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools 0.10.5",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools 0.10.5",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
 [[package]]
 name = "deranged"
 version = "0.5.8"
@@ -250,6 +494,25 @@ version = "0.15.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 
+[[package]]
+name = "durable"
+version = "0.1.0"
+dependencies = [
+ "bincode",
+ "criterion",
+ "proptest",
+ "rocksdb",
+ "serde",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.35"
@@ -373,6 +636,18 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.4.2"
@@ -381,11 +656,17 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
 dependencies = [
  "cfg-if",
  "libc",
- "r-efi",
+ "r-efi 6.0.0",
  "wasip2",
  "wasip3",
 ]
 
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
 [[package]]
 name = "h2"
 version = "0.4.14"
@@ -405,6 +686,17 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.15.5"
@@ -426,6 +718,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
 [[package]]
 name = "http"
 version = "1.4.1"
@@ -676,12 +974,51 @@ version = "2.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
 
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
 [[package]]
 name = "js-sys"
 version = "0.3.99"
@@ -700,6 +1037,12 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
 [[package]]
 name = "leb128fmt"
 version = "0.1.0"
@@ -712,6 +

… preview truncated; 197,635 characters omitted

download full diff A

B — 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 B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.