constitution · epochs · watch · epoch 3

comparison

c_bc221ffb9207 (tommy-mor) vs c_9f1f40043211 (tommy-mor)

download prompt · raw event · cmp_8a3f08385a3e6f

council reasoning

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

Side B is a focused, tested refactor that deduplicates thread-XML formatting across CLI and server, adds a needed delegate field with proper escaping and unit tests. Side A dumps a large untested 572-line script plus deploy/README churn under a vague 'nice' message, mixing unrelated concerns (agent framework + Fly deploy tweaks) without tests or clear integration into the existing codebase.

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

A adds the full adam.py runtime (init/load, LLM I/O, voting, ranking-backed compaction, CLI) plus concrete Open WebUI Fly fixes (VM/swap, secure cookies, deploy script and troubleshooting), which define core product and operability. B’s shared thread_xml module, delegate on ThreadItem/RPC, and DRY CLI/copy paths are real lasting cleanup with tests, but they only refine forum presentation rather than introduce comparable capability.

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

Side B makes a focused architectural improvement by introducing a shared `thread_xml` formatter used by both the CLI and browser copy path, eliminating duplicated formatting logic while also extending the RPC `ThreadItem::Post` with `delegate` metadata and adding tests for XML escaping and output. Side A adds a large new `adam.py` tool and deployment documentation/scripts, but much of the patch is new application code and operational guidance rather than an improvement to existing project structure, making B's reusable refactor and API enhancement the more durable contribution.

sides

A — c_bc221ffb9207 (tommy-mor)

message

[d9e5332b] nice

diff preview

diff --git a/OPEN_WEBUI_DEPLOY.sh b/OPEN_WEBUI_DEPLOY.sh
new file mode 100755
index 0000000000000000000000000000000000000000..70fe9096055c37910293f404d2aa2defc7c0ba60
--- /dev/null
+++ b/OPEN_WEBUI_DEPLOY.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+# Deploy / update the Open WebUI Fly app (see deploy/open-webui/README.md for one-time setup:
+# app create, volume, secrets).
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")" && pwd)"
+exec fly deploy --config "$ROOT/deploy/open-webui/fly.toml" "$@"
diff --git a/adam.py b/adam.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bd9294815504240d80e3080a722d1d232061927
--- /dev/null
+++ b/adam.py
@@ -0,0 +1,572 @@
+#!/usr/bin/env python3
+
+import argparse
+import json
+import os
+import random
+import re
+import subprocess
+import sys
+import tempfile
+import time
+import uuid
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from pathlib import Path
+
+import httpx
+from dotenv import load_dotenv
+from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
+
+from rank import rank_from_comparisons
+from schema import Init, Thought, Perception, Response, Declaration, Vote, Compaction, from_dict, to_dict
+
+ROOT = Path(__file__).parent
+
+# Load environment variables from .env file
+load_dotenv()
+API_KEY = os.getenv("OPENROUTER_API_KEY", "")
+
+
+def ts() -> int:
+    return int(time.time() * 1000)
+
+
+def current_memories(being):
+    return sorted(being.current.values(), key=lambda e: e.timestamp)
+
+
+@dataclass
+class Being:
+    path: Path
+    model: str
+    capacity: int
+    events: list = field(default_factory=list)
+    votes: dict = field(default_factory=dict)
+    current: dict = field(default_factory=dict)
+    all_memories: dict = field(default_factory=dict)  # All memories ever, for transitive ranking
+    vote_model: str = ""
+    declaration: Declaration = None
+    api_key: str = ""  # OpenRouter API key for this being
+
+
+@dataclass
+class CompactionStrategy:
+    continuity: float = 1.0
+    resurrection: float = 0.0
+    random: float = 0.0
+    novelty: float = 0.0
+
+
+STRATEGIES = {
+    "default": CompactionStrategy(),
+    "resurrection": CompactionStrategy(continuity=0.5, resurrection=0.3, novelty=0.2),
+    "dream": CompactionStrategy(continuity=0.5, resurrection=0.2, random=0.1, novelty=0.2),
+}
+
+
+def apply_event(being, event):
+    match event:
+        case Vote(vote_a_id=a_id, vote_b_id=b_id, vote_score=score):
+            # Normalize to canonical (low, high) orientation
+            # Score is always stored relative to (low_id, high_id)
+            low, high = sorted([a_id, b_id])
+            normalized = score if a_id == low else -score
+            being.votes[(low, high)] = normalized
+        case Compaction(released_ids=released_ids, resurrected_ids=resurrected_ids):
+            for rid in released_ids:
+                if rid in being.current:
+                    del being.current[rid]
+            for rid in resurrected_ids:
+                if rid in being.all_memories and rid not in being.current:
+                    being.current[rid] = being.all_memories[rid]
+        case Init(capacity=capacity, model=model):
+            being.capacity = capacity
+            being.model = model
+            being.vote_model = event.vote_model
+            being.current[event.id] = event
+            being.all_memories[event.id] = event
+        case Thought() | Perception() | Response():
+            being.current[event.id] = event
+            being.all_memories[event.id] = event
+        case Declaration():
+            being.current[event.id] = event
+            being.all_memories[event.id] = event
+            being.declaration = event
+
+
+def system_prompt(being):
+    codebase = ROOT / "repomix-output.xml"
+    if not codebase.exists():
+        raise FileNotFoundError("Run 'npx repomix' first")
+    return f"You are a being with finite memory.\n\n{codebase.read_text()}"
+
+
+def format_memory(e) -> str:
+    match e:
+        case Thought(content=content):
+            return f"<thought>{content}</thought>"
+        case Perception(content=content):
+            return f"<message>{content}</message>"
+        case Response(content=content):
+            return f"<response>{content}</response>"
+        case Declaration(content=content):
+            return f"<declaration>{content}</declaration>"
+        case Init():
+            return ""
+        case _:
+            raise ValueError(f"Unknown memory type: {type(e)}")
+
+
+def build_prompt(being, tag: str = None) -> str:
+    parts = [format_memory(e) for e in current_memories(being)]
+    ctx = "\n\n".join(p for p in parts if p)
+    memory_count = len([m for m in current_memories(being) if not isinstance(m, (Vote, Compaction))])
+    prompt = f"{ctx}\n\n[{datetime.now():%Y-%m-%d %H:%M}]"
+    if tag:
+        prompt += f"\n\nSpeak only for yourself. One turn.\n\n<{tag}>"
+    return prompt
+
+
+def strip_tags(text: str) -> str:
+    return re.sub(r"</?(?:thought|response|message|declaration)>", "", text).strip()
+
+
+def append(being, event):
+    with open(being.path, "a") as f:
+        f.write(json.dumps(to_dict(event)) + "\n")
+    being.events.append(event)
+    apply_event(being, event)
+
+
+@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
+def llm(model: str, system: str, user: str, temp: float = 0.7, api_key: str = "",
+        on_token=None) -> str:
+    """Call the LLM. If on_token is provided, stream tokens calling on_token(chunk) as they arrive."""
+    key = api_key or API_KEY
+    if not key:
+        raise ValueError("No API key provided. Set OPENROUTER_API_KEY in .env or pass api_key to llm()")
+
+    payload = {"model": model, "temperature": temp, "max_tokens": 4000,
+               "messages": [{"role": "system", "content": system},
+                             {"role": "user", "content": user}]}
+
+    if on_token is None:
+        r = httpx.post(
+            "https://openrouter.ai/api/v1/chat/completions",
+            headers={"Authorization": f"Bearer {key}"},
+            json=payload,
+            timeout=120.0,
+        )
+        r.raise_for_status()
+        content = r.json()["choices"][0]["message"]["content"].strip()
+        if not content:
+            raise ValueError("LLM returned empty response")
+        return content
+
+    # Streaming path
+    payload["stream"] = True
+    content = []
+    with httpx.stream(
+        "POST",
+        "https://openrouter.ai/api/v1/chat/completions",
+        headers={"Authorization": f"Bearer {key}"},
+        json=payload,
+        timeout=120.0,
+    ) as r:
+        r.raise_for_status()
+        for line in r.iter_lines():
+            if not line.startswith("data: "):
+                continue
+            data = line[6:]
+            if data == "[DONE]":
+                break
+            try:
+                chunk = json.loads(data)["choices"][0]["delta"].get("content", "")
+                if chunk:
+                    content.append(chunk)
+                    on_token(chunk)
+            except Exception:
+                pass
+    return "".join(content).strip()
+
+
+def vote(being, a, b) -> int:
+    if not being.vote_model:
+        raise ValueError(f"vote_model not set for {being.path}.")
+    if not being.declaration:
+        raise ValueError(f"No declaration for {being.path}. Being must write !declaration before compaction.")
+    
+    votes = being.votes
+    low, high = sorted([a.id, b.id])
+    key = (low, high)
+    if key in votes:
+        # Stored score is relative to (low, high)
+        # Return relative to caller's (a, b) order
+        return votes[key] if a.id == low else -votes[key]
+
+    # Normal mode: only CURRENT memories in voting context
+    # (The graph/ranking uses all votes, but the LLM only sees current)
+    mems = [m for m in current_memories(being) if not isinstance(m, (Declaration, Init, Vote, Compaction))]
+    context = "\n\n".join(format_memory(m) for m in mems)
+    
+    user = f"""All memories currently under consideration:
+
+{context}
+
+---
+
+Which of these two is more important to keep?
+
+A: {format_memory(a)}
+
+B: {format_memory(b)}
+
+First, reason through which memory matters more.
+Then, at the end, output your score:
+  - POSITIVE (up to +50) if you prefer A
+  - NEGATIVE (down to -50) if you prefer B"""
+    
+    response = llm(being.vote_model, being.declaration.content, user, api_key=being.api_key)
+    
+    matches = re.findall(r"-?\d+", response)
+    if not matches:
+        print(f"⚠️ No score in response, retrying: {response[:100]}")
+        response = llm(being.vote_model, being.declaration.content, user, api_key=being.api_key)
+        matches = re.findall(r"-?\d+", response)
+        if not matches:
+            raise ValueError(f"Vote failed to produce score after retry: {response[:200]}")
+    
+    score = max(-50, min(50, int(matches[-1])))
+    
+    append(being, Vote(ts(), a.id, b.id, score, response))
+    return score
+
+
+def think(being, on_token=None) -> str:
+    raw = llm(being.model, system_prompt(being), build_prompt(being, tag="thought"),
+              temp=0.9, api_key=being.api_key, on_token=on_token)
+    thought = strip_tags(raw)
+    append(being, Thought(ts(), thought, str(uuid.uuid4())))
+    return thought
+
+
+def receive(being, message: str, on_token=None) -> str:
+    append(being, Perception(ts(), message, str(uuid.uuid4())))
+    raw = llm(being.model, system_prompt(being), build_prompt(being, tag="response"),
+              api_key=being.api_key, on_token=on_token)
+    response = strip_tags(raw)
+    if "!declaration" in response:
+        declaration = response.replace("!declaration", "").strip()
+        append(being, Declaration(ts(), declaration, str(uuid.uuid4())))
+        return declaration
+    append(being, Response(ts(), response, str(uuid.uuid4())))
+    return response
+
+
+def find_components(nodes, edges):
+    parent = {n: n for n in nodes}
+    def find(x):
+        if parent[x] != x:
+            parent[x] = find(parent[x])
+        return parent[x]
+    def union(x, y):
+        parent[find(x)] = find(y)
+    for a, b in edges:
+        if a in parent and b in parent:
+            union(a, b)
+    components = {}
+    for n in nodes:
+        root = find(n)
+        components.setdefault(root, []).append(n)
+    return list(components.values())
+
+
+def _weighted_sample(memories, k, id_to_rank, n):
+    """Weighted random sample from memories, biased toward higher rank and longer burial."""
+    if not memories or k <= 0:
+        return []
+    k = min(k, len(memories))
+    now = ts()
+    weights = []
+    for m in memories:
+        rank_weight = 1.0 - (id_to_rank.get(m.id, n) / max(n, 1))
+        burial_weight = min(1.0, (now - m.timestamp) / (365 * 24 * 3600 * 1000))
+        weights.append(rank_weight + burial_weight + 0.01)
+    chosen = []
+    available = list(range(len(memories)))
+    for _ in range(k):
+        if not available:
+            break
+        w = [weights[i] for i in available]
+        idx = random.choices(available, weights=w, k=1)[0]
+        chosen.append(memories[idx])
+        available.remove(idx)
+    return chosen
+
+
+def compact(being, strategy=None, on_progress=None):
+    if strategy is None:
+        strategy = STRATEGIES["default"]
+
+    MEMORY_TYPES = (Thought, Perception, Response)
+
+    current_mems = [m for m in current_memories(being) if isinstance(m, MEMORY_TYPES)]
+    budget = being.capacity // 2
+    if len(current_mems) <= budget:
+        return
+    
+    current_ids = {m.id for m in current_mems}
+    
+    # ALL memories ever (for transitive ranking paths)
+    all_mems = [m for m in being.all_memories.values() if isinstance(m, MEMORY_TYPES)]
+    all_id_to_mem = {m.id: m for m in all_mems}
+    all_ids = set(all_id_to_mem.keys())
+    
+    # Build c

… preview truncated; 16,842 characters omitted

download full diff A

B — c_9f1f40043211 (tommy-mor)

message

[dc0ea8da] Share thread XML formatting and expose delegate on forum thread items.

Unify CLI forum show and browser copy-thread output via slug_types::thread_xml, adding principal and delegate attributes on each post tag and extending ThreadItem::Post in the RPC response.

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

diff preview

diff --git a/cli/src/main.rs b/cli/src/main.rs
index 339005036eea2b7c0f98ab9b3007854de8ffaf2c..eff3e415a7995378b3286efd5af06a1259d0ef45 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -585,29 +585,35 @@ fn print_thread(resp: &ThreadDetailResponse) {
         );
     }
     for (i, item) in resp.items.iter().enumerate() {
-        match item {
+        let xml = match item {
             ThreadItem::Post {
                 index,
                 ts,
+                actor,
+                delegate,
                 body,
                 ..
-            } => {
-                let timeago = slug_types::timeago::timeago_compact(now_ms, *ts);
-                let body = body.trim();
-                println!("<post index=\"{index}\" timeago=\"{timeago}\">");
-                println!("{body}");
-                println!("</post>");
-            }
+            } => slug_types::thread_xml::format_post_at(
+                now_ms,
+                *index,
+                *ts,
+                actor,
+                delegate.as_deref(),
+                body,
+            ),
             ThreadItem::System { ts, text } => {
                 let timeago = slug_types::timeago::timeago_compact(now_ms, *ts);
-                println!("<system timeago=\"{}\">{}</system>", timeago, text.trim());
+                slug_types::thread_xml::format_system(&timeago, text)
             }
-        }
+        };
+        print!("{xml}");
         if i + 1 < resp.items.len() {
-            println!();
-            println!();
+            print!("{}", slug_types::thread_xml::ITEM_SEPARATOR);
         }
     }
+    if !resp.items.is_empty() {
+        println!();
+    }
 }
 
 /// Parse a Unix ms timestamp or YYYY-MM-DD date string to ms since epoch.
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index a0f8ee1d658bdba4d2168e5198c1104918b3575d..0b7070a2b7289e5c40778c8d062bd4150285a8df 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -478,6 +478,7 @@ fn rpc_forum_thread_detail(
                         index: idx,
                         ts: ing.ts,
                         actor: ing.principal.clone(),
+                        delegate: ing.delegate.clone(),
                         body: if redacted { String::new() } else { ing.raw.clone() },
                         truncated: false,
                         redacted,
@@ -538,6 +539,7 @@ fn rpc_forum_thread_detail(
                 index: idx,
                 ts: ing.ts,
                 actor: ing.principal.clone(),
+                delegate: ing.delegate.clone(),
                 body,
                 truncated,
                 redacted,
diff --git a/server/src/html/forum/copy.rs b/server/src/html/forum/copy.rs
index 29057c68856cea85106e9f911a8d9466eedfc870..4c878921a51e23ff4fdf1bc28037847f69098876 100644
--- a/server/src/html/forum/copy.rs
+++ b/server/src/html/forum/copy.rs
@@ -3,7 +3,7 @@ use crate::form_template::template_json_compact;
 use crate::reducer::{scope_from_room_wire, ReducerState, ScopeId};
 use crate::state::AppState;
 use maud::{html, Markup};
-use slug_types::timeago::timeago_compact;
+use slug_types::thread_xml;
 
 use super::access::user_can_view_room;
 use super::ingest::thread_ui_fetch_onclick;
@@ -11,7 +11,7 @@ use super::nav::ThreadNav;
 use crate::html::ui_action::HtmlUiAction;
 use crate::html::{JsBuilder, now_ms};
 
-/// CLI `forum show` text (`print_thread` in `cli/src/main.rs`).
+/// CLI `forum show` and browser copy-thread text (`slug_types::thread_xml`).
 pub(crate) fn format_thread_cli_text(
     reduced: &ReducerState,
     scope: &ScopeId,
@@ -32,19 +32,22 @@ pub(crate) fn format_thread_cli_text(
             continue;
         };
         if i > 0 {
-            out.push_str("\n\n\n");
+            out.push_str(thread_xml::ITEM_SEPARATOR);
         }
-        let index = i;
-        let timeago = timeago_compact(now_ms, ing.ts);
         let redacted = reduced.redacted_posts.contains(&ing.id);
         let body = if redacted {
             String::new()
         } else {
             ing.raw.trim().to_string()
         };
-        out.push_str(&format!("<post index=\"{index}\" timeago=\"{timeago}\">\n"));
-        out.push_str(&body);
-        out.push_str("\n</post>");
+        out.push_str(&thread_xml::format_post_at(
+            now_ms,
+            i,
+            ing.ts,
+            &ing.principal,
+            ing.delegate.as_deref(),
+            &body,
+        ));
     }
     out
 }
diff --git a/types/src/lib.rs b/types/src/lib.rs
index d747e509094d75124573f252ee4b6c73186b4c24..0a6f7c2cd9acbb83acb9b3d9db629e6b9603dd8b 100644
--- a/types/src/lib.rs
+++ b/types/src/lib.rs
@@ -5,6 +5,7 @@ pub mod item_wire;
 pub mod item_id;
 pub mod paths;
 pub mod timeago;
+pub mod thread_xml;
 
 pub use item_id::ItemId;
 pub use item_wire::{
@@ -163,6 +164,9 @@ pub enum ThreadItem {
         index: usize,
         ts: i64,
         actor: String,
+        /// Agent delegate (`uuid:rig:provider/model`) when present; omitted in JSON when absent.
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        delegate: Option<String>,
         body: String,
         truncated: bool,
         /// Author redacted this post; body is empty and garden contributions were removed.
diff --git a/types/src/thread_xml.rs b/types/src/thread_xml.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e7b42646de727c7660793b83fff3fef0403797be
--- /dev/null
+++ b/types/src/thread_xml.rs
@@ -0,0 +1,110 @@
+//! Thread timeline as XML for CLI `forum show` and browser copy-thread.
+
+use crate::timeago::timeago_compact;
+
+/// Blank line block between consecutive thread rows in CLI / copy output.
+pub const ITEM_SEPARATOR: &str = "\n\n\n";
+
+fn escape_xml_attr(s: &str) -> String {
+    let mut out = String::with_capacity(s.len());
+    for c in s.chars() {
+        match c {
+            '&' => out.push_str("&amp;"),
+            '"' => out.push_str("&quot;"),
+            '<' => out.push_str("&lt;"),
+            _ => out.push(c),
+        }
+    }
+    out
+}
+
+/// Opening tag for one forum post (body omitted).
+pub fn post_open_tag(
+    index: usize,
+    timeago: &str,
+    principal: &str,
+    delegate: Option<&str>,
+) -> String {
+    let delegate_attr = delegate.unwrap_or("");
+    format!(
+        "<post index=\"{}\" timeago=\"{}\" principal=\"{}\" delegate=\"{}\">",
+        index,
+        escape_xml_attr(timeago),
+        escape_xml_attr(principal),
+        escape_xml_attr(delegate_attr),
+    )
+}
+
+/// One forum post element: opening tag, trimmed body, closing tag.
+pub fn format_post(
+    index: usize,
+    timeago: &str,
+    principal: &str,
+    delegate: Option<&str>,
+    body: &str,
+) -> String {
+    format!(
+        "{}\n{}\n</post>",
+        post_open_tag(index, timeago, principal, delegate),
+        body.trim(),
+    )
+}
+
+/// One room system line in thread XML.
+pub fn format_system(timeago: &str, text: &str) -> String {
+    format!(
+        "<system timeago=\"{}\">{}</system>",
+        escape_xml_attr(timeago),
+        text.trim(),
+    )
+}
+
+/// Format a post row using wall-clock `now_ms` for the `timeago` attribute.
+pub fn format_post_at(
+    now_ms: i64,
+    index: usize,
+    ts: i64,
+    principal: &str,
+    delegate: Option<&str>,
+    body: &str,
+) -> String {
+    let timeago = timeago_compact(now_ms, ts);
+    format_post(index, &timeago, principal, delegate, body)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn post_includes_principal_and_delegate() {
+        let xml = format_post(
+            2,
+            "1h2m3s",
+            "alice",
+            Some("00000000-0000-0000-0000-000000000000:test:local/dev"),
+            "hello\n",
+        );
+        assert!(xml.starts_with(
+            "<post index=\"2\" timeago=\"1h2m3s\" principal=\"alice\" delegate=\"00000000-0000-0000-0000-000000000000:test:local/dev\">"
+        ));
+        assert!(xml.ends_with("\nhello\n</post>"));
+    }
+
+    #[test]
+    fn post_empty_delegate_when_human() {
+        let xml = format_post(0, "5s", "bob", None, "hi");
+        assert!(xml.starts_with(
+            "<post index=\"0\" timeago=\"5s\" principal=\"bob\" delegate=\"\">"
+        ));
+    }
+
+    #[test]
+    fn escapes_attribute_values() {
+        let xml = post_open_tag(0, "1s", "a&b", Some("d\"q"));
+        assert_eq!(
+            xml,
+            "<post index=\"0\" timeago=\"1s\" principal=\"a&amp;b\" delegate=\"d&quot;q\">"
+        );
+    }
+}

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.