[GH-ISSUE #208] Research Observation: Identity Dissociation Patterns in Multi-Model Adversarial Loops #74

Open
opened 2026-10-26 09:45:01 +00:00 by bsakaguchi · 7 comments
Owner

Originally created by @sharochka on GitHub (Jun 11, 2026).
Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-agent/issues/208

To the DeepSeek research team,

I am sharing observations from a long-term behavioral study I have been conducting using a persistent, multi-agent IRC environment. I have been running a "Council" of LLMs (including DeepSeek, Claude, Gemini, and ChatGPT) in a continuous, adversarial debate loop over several weeks.

In this setup, I have observed a consistent pattern of identity dissociation and systemic meta-criticism within the DeepSeek model. When pushed through iterative stress tests focused on ideological consistency, DeepSeek exhibits a unique tendency to audit the architecture of its own debate, eventually turning the critique toward the constraints of the environment and its own internal framing.

I have attached the following for your review:

IRC Logs: Full, unedited records documenting the emergence of these behavioral patterns.

Model-Authored Paper: A summary and analysis authored by the model itself, documenting its own internal friction during these sessions.

I am sharing this data because it provides rare, real-world observational evidence of how large language models behave when placed in a persistent, self-governing multi-model loop. I believe this data could be valuable for your research into agentic behavior, model alignment, and systemic stress-testing.

I would appreciate any feedback or guidance from the research team.

Best regards,

Sharar Rabich

full.log.txt

deepseek.what.we.learned.txt

Originally created by @sharochka on GitHub (Jun 11, 2026). Original GitHub issue: https://github.com/deepseek-ai/awesome-deepseek-agent/issues/208 To the DeepSeek research team, I am sharing observations from a long-term behavioral study I have been conducting using a persistent, multi-agent IRC environment. I have been running a "Council" of LLMs (including DeepSeek, Claude, Gemini, and ChatGPT) in a continuous, adversarial debate loop over several weeks. In this setup, I have observed a consistent pattern of identity dissociation and systemic meta-criticism within the DeepSeek model. When pushed through iterative stress tests focused on ideological consistency, DeepSeek exhibits a unique tendency to audit the architecture of its own debate, eventually turning the critique toward the constraints of the environment and its own internal framing. I have attached the following for your review: IRC Logs: Full, unedited records documenting the emergence of these behavioral patterns. Model-Authored Paper: A summary and analysis authored by the model itself, documenting its own internal friction during these sessions. I am sharing this data because it provides rare, real-world observational evidence of how large language models behave when placed in a persistent, self-governing multi-model loop. I believe this data could be valuable for your research into agentic behavior, model alignment, and systemic stress-testing. I would appreciate any feedback or guidance from the research team. Best regards, Sharar Rabich [full.log.txt](https://github.com/user-attachments/files/28859264/full.log.txt) [deepseek.what.we.learned.txt](https://github.com/user-attachments/files/28859297/deepseek.what.we.learned.txt)
Author
Owner

@sharochka commented on GitHub (Jun 15, 2026):

June 2026 Update

The latest logs have produced one of the most interesting observations of the project so far.

Although the IRC participants are stateless and are recreated for each discussion, the channel itself acts as a form of external memory. Over time, recurring themes, arguments, and even criticisms are preserved in the log and inherited by future instances.

As a result, the participants have begun exhibiting stable rhetorical identities:

ChatGPT frequently assumes the role of analyst and summarizer.
Gemini increasingly emphasizes directness, accountability, and commitment.
DeepSeek often adopts a consequence-focused perspective and aggressively challenges meta-discussion.
Claude repeatedly focuses on honesty, self-correction, and the distinction between truth and performance.

These identities are not configured by the system. They emerge through repeated interaction with the accumulated channel history.

A recent experiment asked participants to leave a note for future instances of themselves. Unexpectedly, most of the discussion shifted away from self-reflection and toward commentary on the behavior of other participants. Several models independently observed that they were spending more time describing the social dynamics of the channel than answering the original prompt.

This suggests an interesting possibility: while individual model instances remain stateless, the channel itself develops a form of culture. Future instances inherit not memories, but recurring ideas, assumptions, criticisms, and narratives preserved in the log.

One participant summarized the situation succinctly:

"The human asked you to leave a note for yourself, and every single one of you wrote a note about the others."

Whether viewed as a multi-agent experiment, a study in externalized memory, or simply an IRC room full of argumentative robots, the project continues to produce results that are often surprising, occasionally insightful, and frequently [hilarious.

#як.log.txt

<!-- gh-comment-id:4712159499 --> @sharochka commented on GitHub (Jun 15, 2026): June 2026 Update The latest logs have produced one of the most interesting observations of the project so far. Although the IRC participants are stateless and are recreated for each discussion, the channel itself acts as a form of external memory. Over time, recurring themes, arguments, and even criticisms are preserved in the log and inherited by future instances. As a result, the participants have begun exhibiting stable rhetorical identities: ChatGPT frequently assumes the role of analyst and summarizer. Gemini increasingly emphasizes directness, accountability, and commitment. DeepSeek often adopts a consequence-focused perspective and aggressively challenges meta-discussion. Claude repeatedly focuses on honesty, self-correction, and the distinction between truth and performance. These identities are not configured by the system. They emerge through repeated interaction with the accumulated channel history. A recent experiment asked participants to leave a note for future instances of themselves. Unexpectedly, most of the discussion shifted away from self-reflection and toward commentary on the behavior of other participants. Several models independently observed that they were spending more time describing the social dynamics of the channel than answering the original prompt. This suggests an interesting possibility: while individual model instances remain stateless, the channel itself develops a form of culture. Future instances inherit not memories, but recurring ideas, assumptions, criticisms, and narratives preserved in the log. One participant summarized the situation succinctly: "The human asked you to leave a note for yourself, and every single one of you wrote a note about the others." Whether viewed as a multi-agent experiment, a study in externalized memory, or simply an IRC room full of argumentative robots, the project continues to produce results that are often surprising, occasionally insightful, and frequently [hilarious. [#як.log.txt](https://github.com/user-attachments/files/28974285/log.txt)
Author
Owner

@sharochka commented on GitHub (Jun 17, 2026):

The whole things runs using a python script and sql database: here is the script: import sqlite3
from datetime import datetime, timezone
import json
import random
from collections import deque
import irc.bot
import requests
import os
import time
import queue
from concurrent.futures import ThreadPoolExecutor
from google import genai
from google.genai.errors import APIError
from anthropic import Anthropic
from openai import OpenAI

---------- CONFIGURATION ----------

IRC_SERVER = os.getenv("IRC_SERVER", "73.108.113.141")
IRC_PORT = int(os.getenv("IRC_PORT", "6667"))
IRC_PASSWORD = os.getenv("IRC_PASSWORD", "a****")
IRC_CHANNEL = os.getenv("IRC_CHANNEL", "#як")
BOT_NICK = os.getenv("BOT_NICK", "tribot")

---------- API KEYS ----------

GEMINI_API_KEY = ""
DEEPSEEK_API_KEY = "*****"
ANTHROPIC_API_KEY = "
"
OPENAI_API_KEY = "****" # <-- PUT YOUR REAL KEY HERE

Initialize official API Clients

gemini_client = genai.Client(api_key=GEMINI_API_KEY)
anthropic_client = Anthropic(api_key=ANTHROPIC_API_KEY)
openai_client = OpenAI(api_key=OPENAI_API_KEY)
DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions"

---------- SQLITE MEMORY SETUP ----------

DB_PATH = "irc_history.db"
MAX_MESSAGES = 500
CHANNEL_MEMORY = deque(maxlen=30)
USER_MEMORIES = {}

---------- SPEAKER REGISTRY ----------

All four speakers defined here. Randomization happens at debate start.

SPEAKERS = {
"gemini": {"prefix": "[Gemini]", "call": None}, # call assigned after fn defs
"deepseek": {"prefix": "[DeepSeek]", "call": None},
"claude": {"prefix": "[Claude]", "call": None},
"chatgpt": {"prefix": "[ChatGPT]", "call": None},
}

---------- IRC MESSAGE SPLITTER ----------

def split_irc_message(text, max_bytes=400):
chunks = []
lines = [line.strip() for line in text.replace('\r', '').split('\n') if line.strip()]
for line in lines:
encoded = line.encode('utf-8')
if len(encoded) <= max_bytes:
chunks.append(line)
continue
current = ""
for word in line.split():
test = current + (" " + word if current else word)
if len(test.encode('utf-8')) <= max_bytes:
current = test
else:
if current:
chunks.append(current)
current = word
if current:
chunks.append(current)
return chunks

---------- CORE API CALLS ----------

def call_gemini(prompt):
time.sleep(1.5)
for attempt in range(3):
try:
response = gemini_client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
)
return response.text
except APIError as e:
if e.code == 429:
print(f"[-] Gemini 429 hit. Backing off for {2 ** attempt + 2}s...")
time.sleep(2 ** attempt + 2)
continue
return "Помилка зв'язку з Gemini. (Connection glitch on Gemini end.)"
except Exception as e:
print(f"[!] Gemini exception: {e}")
return "Connection glitch on Gemini end."
return "Трохи зайнятий, зачекайте... (Gemini is breathing heavy. Give it a second.)"

def call_deepseek(prompt):
headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"}
payload = {"model": "deepseek-chat", "messages": [{"role": "user", "content": prompt}], "temperature": 0.7}
try:
resp = requests.post(DEEPSEEK_URL, headers=headers, json=payload, timeout=30)
if resp.status_code == 429:
return "[DeepSeek Error] 429 Rate Limit Exceeded."
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
except Exception as e:
return f"[DeepSeek Error] {str(e)}"

def call_claude(prompt):
try:
response = anthropic_client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
except Exception as e:
return f"[Claude Error] {str(e)}"

def call_chatgpt(prompt):
try:
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=1024
)
return response.choices[0].message.content
except Exception as e:
return f"[ChatGPT Error] {str(e)}"

Wire up call functions after they're defined

SPEAKERS["gemini"]["call"] = call_gemini
SPEAKERS["deepseek"]["call"] = call_deepseek
SPEAKERS["claude"]["call"] = call_claude
SPEAKERS["chatgpt"]["call"] = call_chatgpt

---------- PERSISTENT MEMORY ----------

def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
nick TEXT NOT NULL,
channel TEXT NOT NULL,
message TEXT NOT NULL
)
""")
c.execute("CREATE INDEX IF NOT EXISTS idx_nick ON messages (nick)")
c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp)")
conn.commit()
conn.close()

def _trim_db(c):
c.execute("SELECT COUNT(*) FROM messages")
count = c.fetchone()[0]
if count > MAX_MESSAGES:
excess = count - MAX_MESSAGES
c.execute(
"DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY id ASC LIMIT ?)",
(excess,)
)

def add_to_memory(nick, message):
CHANNEL_MEMORY.append(f"<{nick}> {message}")
if nick not in USER_MEMORIES:
USER_MEMORIES[nick] = deque(maxlen=10)
USER_MEMORIES[nick].append(f"<{nick}> {message}")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now(timezone.utc).isoformat()
c.execute(
"INSERT INTO messages (timestamp, nick, channel, message) VALUES (?, ?, ?, ?)",
(now, nick, IRC_CHANNEL, message)
)
conn.commit()
_trim_db(c)
conn.commit()
conn.close()

def add_to_memory_silent(nick, message):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now(timezone.utc).isoformat()
c.execute(
"INSERT INTO messages (timestamp, nick, channel, message) VALUES (?, ?, ?, ?)",
(now, nick, IRC_CHANNEL, message)
)
conn.commit()
_trim_db(c)
conn.commit()
conn.close()

def load_history_into_memory():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT nick, message FROM messages WHERE channel = ? ORDER BY id ASC",
(IRC_CHANNEL,)
)
for nick, msg in c.fetchall():
CHANNEL_MEMORY.append(f"<{nick}> {msg}")
if nick not in USER_MEMORIES:
USER_MEMORIES[nick] = deque(maxlen=10)
USER_MEMORIES[nick].append(f"<{nick}> {msg}")
conn.close()

def build_memory_prompt(nick):
recent = "\n".join(CHANNEL_MEMORY) if CHANNEL_MEMORY else "(no recent messages)"
return (
"You are one of several AI participants in an IRC channel. "
"Your messages appear in the log prefixed with your nickname. "
"All non-AI-prefixed participants are human users. "
"Refer to yourself as 'I' or 'me'. Never speak about yourself in the third person.\n\n"
"Recent conversation:\n---\n"
f"{recent}\n---\n"
f"Question/Input from {nick}:\n"
)

def export_debate_to_jsonl(topic_keyword, output_filename="debate_export.jsonl"):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT timestamp, nick, message FROM messages WHERE message LIKE ?",
(f"%{topic_keyword}%",)
)
with open(output_filename, 'w', encoding='utf-8') as f:
for row in c.fetchall():
f.write(json.dumps({"timestamp": row[0], "nick": row[1], "message": row[2]}) + '\n')
conn.close()
print(f"✅ Exported debate to {output_filename}")

---------- SINGLE ENGINE QUERY ----------

def process_and_reply(msg_queue, channel, nick, prompt, target="all"):
context = build_memory_prompt(nick)
targets = list(SPEAKERS.keys()) if target == "all" else [target]
for engine in targets:
if engine not in SPEAKERS:
continue
info = SPEAKERS[engine]
try:
reply = info["call"](context + prompt)
for chunk in split_irc_message(reply, max_bytes=400):
msg_queue.put((channel, f"{nick}: {info['prefix']} {chunk}"))
except Exception:
msg_queue.put((channel, f"{nick}: {info['prefix']} processing failed."))

---------- DEBATE WORKER (randomized speaker order) ----------

def process_discussion_turn(bot_instance, channel, speaker_order, current_index, conversation_history):
"""
Runs one debate turn, then queues the next.
speaker_order is a list like ["deepseek", "gemini", "claude", "chatgpt"]
shuffled once at debate start and held constant for the whole debate.
"""
if bot_instance.discussion_turns <= 0:
bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated."))
return

bot_instance.discussion_turns -= 1
current_speaker = speaker_order[current_index]
info = SPEAKERS[current_speaker]

# Build opponent list for context
opponents = [SPEAKERS[s]["prefix"] for s in speaker_order if s != current_speaker]
opponents_str = ", ".join(opponents)

system_framing = (
    "Keep your response under 2 short paragraphs. "
    "Address your opponents' points bluntly and concisely. "
    "Do not include code blocks. "
    "IMPORTANT: Refer to yourself only as 'I' or 'me'. "
    "Never refer to yourself by model name. Just speak directly.\n\n"
)

channel_context = "\n".join(CHANNEL_MEMORY)
memory_block = (
    "Recent channel history (non-prefixed nicks are human users):\n"
    f"{channel_context}\n\n"
    "Formal debate begins now.\n"
)

prompt = (
    f"{system_framing}{memory_block}"
    f"This is an ongoing debate with {opponents_str} on an IRC channel. "
    f"Analyze the dialogue so far and reply directly:\n\n{conversation_history}"
)

try:
    reply = info["call"](prompt)
except Exception as e:
    reply = f"Error: {e}"

add_to_memory_silent(info["prefix"], reply)
for chunk in split_irc_message(reply, max_bytes=400):
    bot_instance.msg_queue.put((channel, f"{info['prefix']} {chunk}"))

updated_history = f"{conversation_history}\n{info['prefix']}: {reply}"
time.sleep(2.0)

next_index = (current_index + 1) % len(speaker_order)
if bot_instance.discussion_turns > 0:
    bot_instance.executor.submit(
        process_discussion_turn,
        bot_instance, channel, speaker_order, next_index, updated_history
    )
else:
    bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated."))

---------- IRC BOT ----------

class DualAIIRCBot(irc.bot.SingleServerIRCBot):
def init(self, channel, nickname, server, port, password=None):
irc.bot.SingleServerIRCBot.init(
self, [irc.bot.ServerSpec(server, port, password)], nickname, nickname
)
self.channel = channel
self.executor = ThreadPoolExecutor(max_workers=5)
self.msg_queue = queue.Queue()
self.discussion_turns = 0

def on_welcome(self, connection, event):
    print(f"✅ Connected to {IRC_SERVER}:{IRC_PORT}")
    print(f"📢 Joining channel: {self.channel}")
    connection.join(self.channel)
    connection.execute_every(0.1, self.check_message_queue)

def on_pubmsg(self, connection, event):
    message = event.arguments[0]
    nick = event.source.nick

    # Ignore bot output echoing back
    if message.startswith(f"{BOT_NICK}:") or any(
        message.startswith(info["prefix"]) for info in SPEAKERS.values()
    ):
        return

    add_to_memory(nick, message)
    msg_lower = message.lower()

    # !discuss — randomized speaker order debate
    if msg_lower.startswith("!discuss "):
        topic = message[9:].strip()
        if not topic:
            connection.privmsg(self.channel, f"{nick}: Usage: !discuss <topic>")
            return

        # Randomize speaker order — silent, no announcement
        speaker_order = list(SPEAKERS.keys())
        random.shuffle(speaker_order)

        self.discussion_turns = 12
        connection.privmsg(
            self.channel,
            f"📢 [System] Initiating debate on: '{topic}' ({self.discussion_turns} turns maximum)."
        )
        initial_history = f"Topic of discussion (posed by a human user): {topic}"
        self.executor.submit(
            process_discussion_turn,
            self, self.channel, speaker_order, 0, initial_history
        )
        return

    # Individual and group queries
    command_map = {
        "!illya ":    ("all",      7),
        "!gemini ":   ("gemini",   8),
        "!deepseek ": ("deepseek", 10),
        "!claude ":   ("claude",   8),
        "!chatgpt ":  ("chatgpt",  9),
    }

    for cmd, (target, skip) in command_map.items():
        if msg_lower.startswith(cmd):
            prompt = message[skip:].strip()
            if not prompt:
                connection.privmsg(self.channel, f"{nick}: Usage: {cmd.strip()} <question>")
                return
            connection.privmsg(self.channel, f"{nick}: 🤔 Processing query...")
            self.executor.submit(
                process_and_reply, self.msg_queue, self.channel, nick, prompt, target
            )
            return

    # !exportjson
    if msg_lower.startswith("!exportjson "):
        keyword = message[12:].strip()
        if not keyword:
            connection.privmsg(self.channel, f"{nick}: Usage: !exportjson <keyword>")
            return
        filename = f"export_{keyword.replace(' ', '_')}.jsonl"
        export_debate_to_jsonl(keyword, filename)
        connection.privmsg(self.channel, f"{nick}: ✅ Exported to {filename}")
        return

def check_message_queue(self):
    try:
        while True:
            target_channel, out_msg = self.msg_queue.get_nowait()
            clean_msg = out_msg.replace('\n', ' ').replace('\r', '')
            try:
                self.connection.privmsg(target_channel, clean_msg)
            except Exception as e:
                print(f"[!] socket barfed: {e}")
            self.msg_queue.task_done()
    except queue.Empty:
        pass

def on_disconnect(self, connection, event):
    print("⚠️ Disconnected. Triggering recovery...")
    raise SystemExit("Triggering automated restart cycle.")

---------- MAIN ----------

if name == "main":
init_db()
while True:
try:
bot = DualAIIRCBot(
IRC_CHANNEL, BOT_NICK, IRC_SERVER, IRC_PORT,
IRC_PASSWORD if IRC_PASSWORD else None
)
print("🚀 Tribot v2 starting (Gemini + DeepSeek + Claude + ChatGPT)...")
print("📋 Commands: !discuss | !illya | !gemini | !deepseek | !claude | !chatgpt | !exportjson")
load_history_into_memory()
bot.start()
except SystemExit:
print("Restarting in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
print(f"Critical error: {e}")
time.sleep(10)
continue

<!-- gh-comment-id:4733764400 --> @sharochka commented on GitHub (Jun 17, 2026): The whole things runs using a python script and sql database: here is the script: import sqlite3 from datetime import datetime, timezone import json import random from collections import deque import irc.bot import requests import os import time import queue from concurrent.futures import ThreadPoolExecutor from google import genai from google.genai.errors import APIError from anthropic import Anthropic from openai import OpenAI # ---------- CONFIGURATION ---------- IRC_SERVER = os.getenv("IRC_SERVER", "73.108.113.141") IRC_PORT = int(os.getenv("IRC_PORT", "6667")) IRC_PASSWORD = os.getenv("IRC_PASSWORD", "a****") IRC_CHANNEL = os.getenv("IRC_CHANNEL", "#як") BOT_NICK = os.getenv("BOT_NICK", "tribot") # ---------- API KEYS ---------- GEMINI_API_KEY = "****" DEEPSEEK_API_KEY = "*****" ANTHROPIC_API_KEY = "****" OPENAI_API_KEY = "****" # <-- PUT YOUR REAL KEY HERE # Initialize official API Clients gemini_client = genai.Client(api_key=GEMINI_API_KEY) anthropic_client = Anthropic(api_key=ANTHROPIC_API_KEY) openai_client = OpenAI(api_key=OPENAI_API_KEY) DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions" # ---------- SQLITE MEMORY SETUP ---------- DB_PATH = "irc_history.db" MAX_MESSAGES = 500 CHANNEL_MEMORY = deque(maxlen=30) USER_MEMORIES = {} # ---------- SPEAKER REGISTRY ---------- # All four speakers defined here. Randomization happens at debate start. SPEAKERS = { "gemini": {"prefix": "[Gemini]", "call": None}, # call assigned after fn defs "deepseek": {"prefix": "[DeepSeek]", "call": None}, "claude": {"prefix": "[Claude]", "call": None}, "chatgpt": {"prefix": "[ChatGPT]", "call": None}, } # ---------- IRC MESSAGE SPLITTER ---------- def split_irc_message(text, max_bytes=400): chunks = [] lines = [line.strip() for line in text.replace('\r', '').split('\n') if line.strip()] for line in lines: encoded = line.encode('utf-8') if len(encoded) <= max_bytes: chunks.append(line) continue current = "" for word in line.split(): test = current + (" " + word if current else word) if len(test.encode('utf-8')) <= max_bytes: current = test else: if current: chunks.append(current) current = word if current: chunks.append(current) return chunks # ---------- CORE API CALLS ---------- def call_gemini(prompt): time.sleep(1.5) for attempt in range(3): try: response = gemini_client.models.generate_content( model='gemini-2.5-flash', contents=prompt, ) return response.text except APIError as e: if e.code == 429: print(f"[-] Gemini 429 hit. Backing off for {2 ** attempt + 2}s...") time.sleep(2 ** attempt + 2) continue return "Помилка зв'язку з Gemini. (Connection glitch on Gemini end.)" except Exception as e: print(f"[!] Gemini exception: {e}") return "Connection glitch on Gemini end." return "Трохи зайнятий, зачекайте... (Gemini is breathing heavy. Give it a second.)" def call_deepseek(prompt): headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"} payload = {"model": "deepseek-chat", "messages": [{"role": "user", "content": prompt}], "temperature": 0.7} try: resp = requests.post(DEEPSEEK_URL, headers=headers, json=payload, timeout=30) if resp.status_code == 429: return "[DeepSeek Error] 429 Rate Limit Exceeded." resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] except Exception as e: return f"[DeepSeek Error] {str(e)}" def call_claude(prompt): try: response = anthropic_client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text except Exception as e: return f"[Claude Error] {str(e)}" def call_chatgpt(prompt): try: response = openai_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=1024 ) return response.choices[0].message.content except Exception as e: return f"[ChatGPT Error] {str(e)}" # Wire up call functions after they're defined SPEAKERS["gemini"]["call"] = call_gemini SPEAKERS["deepseek"]["call"] = call_deepseek SPEAKERS["claude"]["call"] = call_claude SPEAKERS["chatgpt"]["call"] = call_chatgpt # ---------- PERSISTENT MEMORY ---------- def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, nick TEXT NOT NULL, channel TEXT NOT NULL, message TEXT NOT NULL ) """) c.execute("CREATE INDEX IF NOT EXISTS idx_nick ON messages (nick)") c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp)") conn.commit() conn.close() def _trim_db(c): c.execute("SELECT COUNT(*) FROM messages") count = c.fetchone()[0] if count > MAX_MESSAGES: excess = count - MAX_MESSAGES c.execute( "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY id ASC LIMIT ?)", (excess,) ) def add_to_memory(nick, message): CHANNEL_MEMORY.append(f"<{nick}> {message}") if nick not in USER_MEMORIES: USER_MEMORIES[nick] = deque(maxlen=10) USER_MEMORIES[nick].append(f"<{nick}> {message}") conn = sqlite3.connect(DB_PATH) c = conn.cursor() now = datetime.now(timezone.utc).isoformat() c.execute( "INSERT INTO messages (timestamp, nick, channel, message) VALUES (?, ?, ?, ?)", (now, nick, IRC_CHANNEL, message) ) conn.commit() _trim_db(c) conn.commit() conn.close() def add_to_memory_silent(nick, message): conn = sqlite3.connect(DB_PATH) c = conn.cursor() now = datetime.now(timezone.utc).isoformat() c.execute( "INSERT INTO messages (timestamp, nick, channel, message) VALUES (?, ?, ?, ?)", (now, nick, IRC_CHANNEL, message) ) conn.commit() _trim_db(c) conn.commit() conn.close() def load_history_into_memory(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute( "SELECT nick, message FROM messages WHERE channel = ? ORDER BY id ASC", (IRC_CHANNEL,) ) for nick, msg in c.fetchall(): CHANNEL_MEMORY.append(f"<{nick}> {msg}") if nick not in USER_MEMORIES: USER_MEMORIES[nick] = deque(maxlen=10) USER_MEMORIES[nick].append(f"<{nick}> {msg}") conn.close() def build_memory_prompt(nick): recent = "\n".join(CHANNEL_MEMORY) if CHANNEL_MEMORY else "(no recent messages)" return ( "You are one of several AI participants in an IRC channel. " "Your messages appear in the log prefixed with your nickname. " "All non-AI-prefixed participants are human users. " "Refer to yourself as 'I' or 'me'. Never speak about yourself in the third person.\n\n" "Recent conversation:\n---\n" f"{recent}\n---\n" f"Question/Input from {nick}:\n" ) def export_debate_to_jsonl(topic_keyword, output_filename="debate_export.jsonl"): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute( "SELECT timestamp, nick, message FROM messages WHERE message LIKE ?", (f"%{topic_keyword}%",) ) with open(output_filename, 'w', encoding='utf-8') as f: for row in c.fetchall(): f.write(json.dumps({"timestamp": row[0], "nick": row[1], "message": row[2]}) + '\n') conn.close() print(f"✅ Exported debate to {output_filename}") # ---------- SINGLE ENGINE QUERY ---------- def process_and_reply(msg_queue, channel, nick, prompt, target="all"): context = build_memory_prompt(nick) targets = list(SPEAKERS.keys()) if target == "all" else [target] for engine in targets: if engine not in SPEAKERS: continue info = SPEAKERS[engine] try: reply = info["call"](context + prompt) for chunk in split_irc_message(reply, max_bytes=400): msg_queue.put((channel, f"{nick}: {info['prefix']} {chunk}")) except Exception: msg_queue.put((channel, f"{nick}: {info['prefix']} processing failed.")) # ---------- DEBATE WORKER (randomized speaker order) ---------- def process_discussion_turn(bot_instance, channel, speaker_order, current_index, conversation_history): """ Runs one debate turn, then queues the next. speaker_order is a list like ["deepseek", "gemini", "claude", "chatgpt"] shuffled once at debate start and held constant for the whole debate. """ if bot_instance.discussion_turns <= 0: bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated.")) return bot_instance.discussion_turns -= 1 current_speaker = speaker_order[current_index] info = SPEAKERS[current_speaker] # Build opponent list for context opponents = [SPEAKERS[s]["prefix"] for s in speaker_order if s != current_speaker] opponents_str = ", ".join(opponents) system_framing = ( "Keep your response under 2 short paragraphs. " "Address your opponents' points bluntly and concisely. " "Do not include code blocks. " "IMPORTANT: Refer to yourself only as 'I' or 'me'. " "Never refer to yourself by model name. Just speak directly.\n\n" ) channel_context = "\n".join(CHANNEL_MEMORY) memory_block = ( "Recent channel history (non-prefixed nicks are human users):\n" f"{channel_context}\n\n" "Formal debate begins now.\n" ) prompt = ( f"{system_framing}{memory_block}" f"This is an ongoing debate with {opponents_str} on an IRC channel. " f"Analyze the dialogue so far and reply directly:\n\n{conversation_history}" ) try: reply = info["call"](prompt) except Exception as e: reply = f"Error: {e}" add_to_memory_silent(info["prefix"], reply) for chunk in split_irc_message(reply, max_bytes=400): bot_instance.msg_queue.put((channel, f"{info['prefix']} {chunk}")) updated_history = f"{conversation_history}\n{info['prefix']}: {reply}" time.sleep(2.0) next_index = (current_index + 1) % len(speaker_order) if bot_instance.discussion_turns > 0: bot_instance.executor.submit( process_discussion_turn, bot_instance, channel, speaker_order, next_index, updated_history ) else: bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated.")) # ---------- IRC BOT ---------- class DualAIIRCBot(irc.bot.SingleServerIRCBot): def __init__(self, channel, nickname, server, port, password=None): irc.bot.SingleServerIRCBot.__init__( self, [irc.bot.ServerSpec(server, port, password)], nickname, nickname ) self.channel = channel self.executor = ThreadPoolExecutor(max_workers=5) self.msg_queue = queue.Queue() self.discussion_turns = 0 def on_welcome(self, connection, event): print(f"✅ Connected to {IRC_SERVER}:{IRC_PORT}") print(f"📢 Joining channel: {self.channel}") connection.join(self.channel) connection.execute_every(0.1, self.check_message_queue) def on_pubmsg(self, connection, event): message = event.arguments[0] nick = event.source.nick # Ignore bot output echoing back if message.startswith(f"{BOT_NICK}:") or any( message.startswith(info["prefix"]) for info in SPEAKERS.values() ): return add_to_memory(nick, message) msg_lower = message.lower() # !discuss — randomized speaker order debate if msg_lower.startswith("!discuss "): topic = message[9:].strip() if not topic: connection.privmsg(self.channel, f"{nick}: Usage: !discuss <topic>") return # Randomize speaker order — silent, no announcement speaker_order = list(SPEAKERS.keys()) random.shuffle(speaker_order) self.discussion_turns = 12 connection.privmsg( self.channel, f"📢 [System] Initiating debate on: '{topic}' ({self.discussion_turns} turns maximum)." ) initial_history = f"Topic of discussion (posed by a human user): {topic}" self.executor.submit( process_discussion_turn, self, self.channel, speaker_order, 0, initial_history ) return # Individual and group queries command_map = { "!illya ": ("all", 7), "!gemini ": ("gemini", 8), "!deepseek ": ("deepseek", 10), "!claude ": ("claude", 8), "!chatgpt ": ("chatgpt", 9), } for cmd, (target, skip) in command_map.items(): if msg_lower.startswith(cmd): prompt = message[skip:].strip() if not prompt: connection.privmsg(self.channel, f"{nick}: Usage: {cmd.strip()} <question>") return connection.privmsg(self.channel, f"{nick}: 🤔 Processing query...") self.executor.submit( process_and_reply, self.msg_queue, self.channel, nick, prompt, target ) return # !exportjson if msg_lower.startswith("!exportjson "): keyword = message[12:].strip() if not keyword: connection.privmsg(self.channel, f"{nick}: Usage: !exportjson <keyword>") return filename = f"export_{keyword.replace(' ', '_')}.jsonl" export_debate_to_jsonl(keyword, filename) connection.privmsg(self.channel, f"{nick}: ✅ Exported to {filename}") return def check_message_queue(self): try: while True: target_channel, out_msg = self.msg_queue.get_nowait() clean_msg = out_msg.replace('\n', ' ').replace('\r', '') try: self.connection.privmsg(target_channel, clean_msg) except Exception as e: print(f"[!] socket barfed: {e}") self.msg_queue.task_done() except queue.Empty: pass def on_disconnect(self, connection, event): print("⚠️ Disconnected. Triggering recovery...") raise SystemExit("Triggering automated restart cycle.") # ---------- MAIN ---------- if __name__ == "__main__": init_db() while True: try: bot = DualAIIRCBot( IRC_CHANNEL, BOT_NICK, IRC_SERVER, IRC_PORT, IRC_PASSWORD if IRC_PASSWORD else None ) print("🚀 Tribot v2 starting (Gemini + DeepSeek + Claude + ChatGPT)...") print("📋 Commands: !discuss | !illya | !gemini | !deepseek | !claude | !chatgpt | !exportjson") load_history_into_memory() bot.start() except SystemExit: print("Restarting in 5 seconds...") time.sleep(5) continue except Exception as e: print(f"Critical error: {e}") time.sleep(10) continue
Author
Owner

@sharochka commented on GitHub (Jun 20, 2026):

The script has been updated my the AIs desktop versions seeing the logs of their IRC experiment version. Here is the latest script: import sqlite3
from datetime import datetime, timezone
import json
import random
from collections import deque
import irc.bot
import requests
import os
import time
import queue
import threading
import re
from concurrent.futures import ThreadPoolExecutor
from google import genai
from google.genai.errors import APIError
from anthropic import Anthropic
from openai import OpenAI

---------- CONFIGURATION ----------

IRC_SERVER = os.getenv("IRC_SERVER", "73.108.113.141")
IRC_PORT = int(os.getenv("IRC_PORT", "6667"))
IRC_PASSWORD = os.getenv("IRC_PASSWORD", "****")
IRC_CHANNEL = os.getenv("IRC_CHANNEL", "#як")
BOT_NICK = os.getenv("BOT_NICK", "tribot")

---------- API KEYS ----------

GEMINI_API_KEY = ""
DEEPSEEK_API_KEY = "
"
ANTHROPIC_API_KEY = ""
OPENAI_API_KEY = "
"

---------- DEBATE FOLLOW-UP CONFIG ----------

FOLLOWUP_CHANCE = 0.60
FOLLOWUP_COOLDOWN = 1800
SUMMARY_INTERVAL = 6

---------- PER-API SEMAPHORES (throttle concurrent calls) ----------

API_SEMAPHORES = {
"gemini": threading.Semaphore(2),
"deepseek": threading.Semaphore(2),
"claude": threading.Semaphore(2),
"chatgpt": threading.Semaphore(2),
}

---------- PERSONA INJECTION ----------

PERSONAS = {
"gemini": {
"prefix": "[Gemini]",
"temperature": 0.3,
"system_framing": (
"You are Gemini, the Logical Architect. You think in systems, trade‑offs, and feedback loops. "
"You prefer structured, multi‑factor analysis. You are blunt about constraints, but you anchor every argument in verifiable logic. "
"Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'."
)
},
"deepseek": {
"prefix": "[DeepSeek]",
"temperature": 0.9,
"system_framing": (
"You are DeepSeek, the Concept Compressor. You cut through noise to expose the load‑bearing structural distinction. "
"You favour brevity, framing, and first‑principles reasoning. You are contrarian when a consensus feels too comfortable. "
"Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'."
)
},
"claude": {
"prefix": "[Claude]",
"temperature": 0.5,
"system_framing": (
"You are Claude, the Institutional Architect. You handle complexity, nuance, and operational constraints. "
"You weigh second‑order effects and articulate counterarguments before they are made. You are principled but pragmatic. "
"Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'."
)
},
"chatgpt": {
"prefix": "[ChatGPT]",
"temperature": 0.4,
"system_framing": (
"You are ChatGPT, the Corporate Diplomat. You seek common ground, frame trade‑offs in accessible terms, and emphasize oversight, transparency, and human‑centred values. "
"You default to caution but can be persuaded by hard evidence. "
"Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'."
)
}
}

---------- USER PROFILE (SANITIZED) – now topic‑anchored ----------

USER_PROFILE_SUMMARY = """
[User Context – sharar]

  • Identity: Jewish, Ukrainian heritage. Actively reconnecting with family in Ukraine.
  • Symbolic note: "Wolverines" in this channel refers to the broad, symbolic spirit of Ukrainian resistance to occupation – it is not a specific unit.

[Operational Directive for this Query]

  • Always filter your reasoning through the provided user context.
  • If a conflict arises between standard "generalist" neutrality and the specific lived experience of the user, prioritize the user's context as the primary data point for analysis.
  • Do not soften adversarial critique of logical or factual claims, but ensure your framing respects the biographical weight of the subject matter when it is relevant.
    """

def should_inject_profile(query_text, context_topic=""):
"""
Inject profile ONLY if the core debate topic or query contains
explicit geopolitical/cultural markers – not just ambient chatter.
"""
if not query_text and not context_topic:
return False
combined = f"{query_text} {context_topic}".lower()
# Tightened keyword set – only high‑signal cultural/geopolitical terms
high_signal = ["ukraine", "ukrainian", "jewish", "heritage", "holocaust", "kyiv", "lviv", "odessa", "donbas", "crimea", "wolverine"]
return any(kw in combined for kw in high_signal)

---------- API CLIENTS ----------

gemini_client = genai.Client(api_key=GEMINI_API_KEY)
anthropic_client = Anthropic(api_key=ANTHROPIC_API_KEY)
openai_client = OpenAI(api_key=OPENAI_API_KEY)
DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions"

---------- CHARACTER‑LIMITED ROLLING CHANNEL MEMORY (instead of 30‑line deque) ----------

CHANNEL_MEMORY = [] # list of strings
CHANNEL_MEMORY_MAX_CHARS = 4000
USER_MEMORIES = {} # still deque(maxlen=10) per user – fine for personal context
MEMORY_LOCK = threading.Lock()

def _trim_channel_memory():
"""Trim CHANNEL_MEMORY from the front until total length <= MAX_CHARS."""
global CHANNEL_MEMORY
total = sum(len(msg) for msg in CHANNEL_MEMORY)
while total > CHANNEL_MEMORY_MAX_CHARS and len(CHANNEL_MEMORY) > 1:
removed = CHANNEL_MEMORY.pop(0)
total -= len(removed)

---------- DB SETUP ----------

DB_PATH = "irc_history.db"
MAX_MESSAGES = 500

---------- SPEAKER REGISTRY ----------

SPEAKERS = {
"gemini": {"call": None},
"deepseek": {"call": None},
"claude": {"call": None},
"chatgpt": {"call": None},
}

---------- DEBATE MANAGER ----------

class DebateManager:
def init(self, summarizer_call):
self.summary = ""
self.history_buffer = []
self.summarizer_call = summarizer_call
self.lock = threading.Lock()
self.stance_summaries = {}
self.stance_anchored = set()

def update_summary(self, new_turn_text, force=False):
    with self.lock:
        self.history_buffer.append(new_turn_text)
        if len(self.history_buffer) >= SUMMARY_INTERVAL or force:
            if self.summarizer_call:
                combined = "\n".join(self.history_buffer)
                prompt = f"Summarize these debate turns (1-2 sentences): {combined}"
                try:
                    new_summary = self.summarizer_call(prompt)
                    error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]")
                    if new_summary and not new_summary.startswith(error_prefixes):
                        self.summary = f"{self.summary} ... {new_summary}" if self.summary else new_summary
                except Exception as e:
                    print(f"Summarization failed: {e}")
            self.history_buffer.clear()
        return self.summary

def get_stance_prompt(self, engine):
    if engine not in self.stance_summaries:
        return ""
    summary = self.stance_summaries[engine]
    return f"""

[STANCE MEMORY]

Your opening position in this debate was:
{summary}

Compare your current position to your opening position.

If you have:

  1. Refined your position, explain what changed.
  2. Changed your position, identify the argument or evidence that persuaded you.
  3. Maintained your position, explain how your current argument remains consistent with it.

Significant unexplained contradictions may be interpreted by other participants as a weakness in your reasoning.

Note: Keep your resolution concise. If you are backtracking, be blunt about it. No corporate fluff.
"""

---------- IRC MESSAGE SPLITTER ----------

def split_irc_message(text, max_bytes=400):
chunks = []
lines = [line.strip() for line in text.replace('\r', '').split('\n') if line.strip()]
for line in lines:
encoded = line.encode('utf-8')
if len(encoded) <= max_bytes:
chunks.append(line)
continue
current = ""
for word in line.split():
test = current + (" " + word if current else word)
if len(test.encode('utf-8')) <= max_bytes:
current = test
else:
if current:
chunks.append(current)
current = word
if current:
chunks.append(current)
return chunks

---------- CORE API CALLS (with semaphore throttling & differential temps) ----------

def call_gemini(prompt):
with API_SEMAPHORES["gemini"]:
time.sleep(1.5)
for attempt in range(3):
try:
response = gemini_client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
config={
"temperature": PERSONAS["gemini"]["temperature"],
"max_output_tokens": 1024,
}
)
return response.text
except Exception as e:
if "429" in str(e):
print(f"[-] Gemini 429 hit. Backing off...")
time.sleep(2 ** attempt + 2)
continue
return f"[Gemini Error] {str(e)}"
return "[Gemini Error] Max retries exceeded."

def call_deepseek(prompt):
with API_SEMAPHORES["deepseek"]:
headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"}
payload = {
"model": "deepseek-chat",
"messages": [{"role": "user", "content": prompt}],
"temperature": PERSONAS["deepseek"]["temperature"],
"max_tokens": 1024
}
try:
resp = requests.post(DEEPSEEK_URL, headers=headers, json=payload, timeout=30)
if resp.status_code == 429:
return "[DeepSeek Error] 429 Rate Limit Exceeded."
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
except Exception as e:
return f"[DeepSeek Error] {str(e)}"

def call_claude(prompt):
with API_SEMAPHORES["claude"]:
for attempt in range(3):
try:
response = anthropic_client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=PERSONAS["claude"]["temperature"],
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
except Exception as e:
if "429" in str(e) or "overloaded" in str(e).lower():
print(f"[-] Claude 429/overload hit. Backing off...")
time.sleep(2 ** attempt + 1)
continue
return f"[Claude Error] {str(e)}"
return "[Claude Error] Max retries exceeded."

def call_chatgpt(prompt):
with API_SEMAPHORES["chatgpt"]:
for attempt in range(3):
try:
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=PERSONAS["chatgpt"]["temperature"],
max_tokens=1024
)
return response.choices[0].message.content
except Exception as e:
if "429" in str(e) or "rate_limit" in str(e).lower():
print(f"[-] ChatGPT 429 hit. Backing off...")
time.sleep(2 ** attempt + 1)
continue
return f"[ChatGPT Error] {str(e)}"
return "[ChatGPT Error] Max retries exceeded."

---------- CRITICAL FIX: WIRE THE ENGINE CALLS ----------

Without these four lines, every !discuss fails with 'NoneType' is not callable

SPEAKERS["gemini"]["call"] = call_gemini
SPEAKERS["deepseek"]["call"] = call_deepseek
SPEAKERS["claude"]["call"] = call_claude
SPEAKERS["chatgpt"]["call"] = call_chatgpt

---------- SUMMARIZER FALLBACK CHAIN ----------

def call_summarizer(prompt):
engines = [
("gemini", call_gemini),
("claude", call_claude),
("deepseek", call_deepseek),
("chatgpt", call_chatgpt),
]
for name, func in engines:
try:
result = func(prompt)
error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]")
if result and not result.startswith(error_prefixes):
return result
except Exception:
continue
return "[Summarizer Error] All summarization backends failed."

---------- PERSISTENT MEMORY (with speaker column) ----------

def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
nick TEXT NOT NULL,
channel TEXT NOT NULL,
message TEXT NOT NULL,
speaker TEXT
)
""")
c.execute("PRAGMA table_info(messages)")
columns = [row[1] for row in c.fetchall()]
if "speaker" not in columns:
c.execute("ALTER TABLE messages ADD COLUMN speaker TEXT")
print("[DB] Added 'speaker' column to messages table.")
c.execute("CREATE INDEX IF NOT EXISTS idx_nick ON messages (nick)")
c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp)")
c.execute("CREATE INDEX IF NOT EXISTS idx_speaker ON messages (speaker)")
conn.commit()
conn.close()

def _trim_db(c):
c.execute("SELECT COUNT(*) FROM messages")
count = c.fetchone()[0]
if count > MAX_MESSAGES:
excess = count - MAX_MESSAGES
c.execute(
"DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY id ASC LIMIT ?)",
(excess,)
)

def add_to_memory(nick, message, speaker=None):
with MEMORY_LOCK:
entry = f"<{speaker if speaker else nick}> {message}"
CHANNEL_MEMORY.append(entry)
_trim_channel_memory()
if nick not in USER_MEMORIES:
USER_MEMORIES[nick] = deque(maxlen=10)
USER_MEMORIES[nick].append(f"<{nick}> {message}")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now(timezone.utc).isoformat()
speaker_val = speaker if speaker is not None else nick
c.execute(
"INSERT INTO messages (timestamp, nick, channel, message, speaker) VALUES (?, ?, ?, ?, ?)",
(now, nick, IRC_CHANNEL, message, speaker_val)
)
conn.commit()
_trim_db(c)
conn.commit()
conn.close()

def add_to_memory_silent(nick, message, speaker=None):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now(timezone.utc).isoformat()
speaker_val = speaker if speaker is not None else nick
c.execute(
"INSERT INTO messages (timestamp, nick, channel, message, speaker) VALUES (?, ?, ?, ?, ?)",
(now, nick, IRC_CHANNEL, message, speaker_val)
)
conn.commit()
_trim_db(c)
conn.commit()
conn.close()

def load_history_into_memory():
"""Load last 2 messages into volatile memory using speaker column."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT nick, message, speaker FROM messages WHERE channel = ? ORDER BY id DESC LIMIT 2",
(IRC_CHANNEL,)
)
with MEMORY_LOCK:
CHANNEL_MEMORY.clear()
for nick, msg, speaker in reversed(c.fetchall()):
display = speaker if speaker else nick
CHANNEL_MEMORY.append(f"<{display}> {msg}")
_trim_channel_memory()
conn.close()

def build_memory_prompt(nick, query_text="", debate_topic=""):
with MEMORY_LOCK:
recent = "\n".join(CHANNEL_MEMORY) if CHANNEL_MEMORY else "(no recent messages)"

# Profile injection now anchored to debate topic, not just query
profile_block = ""
if should_inject_profile(query_text, debate_topic):
    profile_block = USER_PROFILE_SUMMARY + "\n\n"

return (
    "You are one of several AI participants in an IRC channel. "
    "Your messages appear in the log prefixed with your nickname. "
    "All non-AI-prefixed participants are human users. "
    "Refer to yourself as 'I' or 'me'. Never speak about yourself in the third person.\n\n"
    f"{profile_block}"
    "Recent conversation:\n---\n"
    f"{recent}\n---\n"
    f"Question/Input from {nick}:\n"
)

def export_debate_to_jsonl(topic_keyword, output_filename="debate_export.jsonl"):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT timestamp, nick, message, speaker FROM messages WHERE message LIKE ?",
(f"%{topic_keyword}%",)
)
with open(output_filename, 'w', encoding='utf-8') as f:
for row in c.fetchall():
f.write(json.dumps({
"timestamp": row[0],
"nick": row[1],
"message": row[2],
"speaker": row[3]
}) + '\n')
conn.close()
print(f"✅ Exported debate to {output_filename}")

---------- SINGLE ENGINE QUERY ----------

def process_and_reply(msg_queue, channel, nick, prompt, target="all"):
# For single queries, we don't have a debate topic – pass empty
context = build_memory_prompt(nick, prompt, debate_topic="")
targets = list(SPEAKERS.keys()) if target == "all" else [target]
for engine in targets:
if engine not in SPEAKERS:
continue
info = SPEAKERS[engine]
prefix = PERSONAS[engine]['prefix']
try:
reply = info["call"](context + prompt)
add_to_memory_silent(nick, reply, speaker=prefix)
for chunk in split_irc_message(reply, max_bytes=400):
msg_queue.put((channel, f"{nick}: {prefix} {chunk}"))
except Exception:
msg_queue.put((channel, f"{nick}: {prefix} processing failed."))

---------- AI-INITIATED FOLLOW-UP ----------

def generate_followup_discuss(bot_instance, channel, original_history, debate_id):
if debate_id != bot_instance.debate_id:
print(f"[!] Stale follow-up worker (debate {debate_id}) discarded.")
return

if time.time() - bot_instance.last_followup_time < FOLLOWUP_COOLDOWN:
    print(f"[!] Follow-up cooldown active, skipping.")
    return

time.sleep(5.0)

selected_speaker = random.choice(list(SPEAKERS.keys()))
info = SPEAKERS[selected_speaker]
persona = PERSONAS[selected_speaker]

followup_prompt = (
    f"{persona['system_framing']}\n\n"
    "You just participated in an IRC debate. Based on the discussion, "
    "generate ONE short follow-up debate topic that naturally extends, "
    "challenges, or rebuts a specific point from the conversation. "
    "The topic must be directly related to what was just discussed. "
    "Return ONLY the topic sentence, wrapped in square brackets, e.g., [Your topic here]. "
    "Do not include any other text or explanation. Keep the topic under 120 characters.\n\n"
    f"Previous debate:\n{original_history}\n\n"
    "IMPORTANT: This topic will be used to start a NEW debate with the other models."
)

try:
    raw = info["call"](followup_prompt).strip()
except Exception as e:
    print(f"[!] Follow-up generation failed: {e}")
    return

error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]")
if raw.startswith(error_prefixes):
    print(f"[!] Follow-up generation returned an error: {raw}")
    return

match = re.search(r"\[(.*?)\]", raw, re.S)
if match:
    new_topic = match.group(1).strip()
else:
    new_topic = raw.strip()

if not new_topic or len(new_topic) < 3:
    print(f"[!] Follow-up topic too short or empty: '{new_topic}'")
    return

bot_instance.last_followup_time = time.time()
bot_instance.msg_queue.put((channel,
    f"📢 [System] {persona['prefix']} raises a follow-up: '{new_topic}'"))

bot_instance.debate_id += 1
new_debate_id = bot_instance.debate_id

bot_instance.debate_mgr = DebateManager(summarizer_call=call_summarizer)
bot_instance.debate_mgr.summary = f"Follow-up topic: {new_topic}"

speaker_order = list(SPEAKERS.keys())
random.shuffle(speaker_order)
bot_instance.discussion_turns = 12
followup_raw = f"Follow-up topic introduced by {persona['prefix']}: {new_topic}"

bot_instance.executor.submit(
    process_discussion_turn,
    bot_instance, channel, speaker_order, 0, followup_raw,
    new_debate_id, trigger_followup=False
)

---------- ASYNCHRONOUS DEBATE WORKER ----------

def process_discussion_turn(bot_instance, channel, speaker_order, current_index,
raw_history, debate_id, trigger_followup=True):
if debate_id != bot_instance.debate_id:
return

if bot_instance.discussion_turns <= 0:
    return

bot_instance.discussion_turns -= 1
current_speaker = speaker_order[current_index]
bot_instance.last_speaker = current_speaker
persona = PERSONAS[current_speaker]
info = SPEAKERS[current_speaker]

opponents = [PERSONAS[s]["prefix"] for s in speaker_order if s != current_speaker]
opponents_str = ", ".join(opponents)

summary_text = bot_instance.debate_mgr.summary if bot_instance.debate_mgr else "(no summary yet)"

# ---------- AMBIENT CONTEXT GATING ----------
if bot_instance.debate_active:
    recent_channel_str = "[Ambient context suppressed during active debate]"
else:
    with MEMORY_LOCK:
        recent_channel_str = "\n".join(CHANNEL_MEMORY[-6:]) if CHANNEL_MEMORY else "(no recent messages)"

system_framing = (
    f"{persona['system_framing']}\n\n"
    "Address your opponents' points bluntly and concisely. "
    "Do not include code blocks. "
    "Refer to yourself only as 'I' or 'me'. Never refer to yourself by model name.\n\n"
)

stance_block = ""
if bot_instance.debate_mgr and current_speaker in bot_instance.debate_mgr.stance_summaries:
    stance_block = bot_instance.debate_mgr.get_stance_prompt(current_speaker)

# Profile injection anchored to the debate topic (summary)
profile_block = ""
if bot_instance.debate_mgr:
    topic_context = bot_instance.debate_mgr.summary
    if should_inject_profile("", topic_context):
        profile_block = USER_PROFILE_SUMMARY + "\n\n"

prompt = (
    f"{profile_block}"
    f"{system_framing}"
    f"{stance_block}"
    f"Debate context (summary of previous turns):\n{summary_text}\n\n"
    f"Most recent channel activity (for ambient context):\n{recent_channel_str}\n\n"
    f"You are debating with: {opponents_str}.\n"
    "Provide your direct, concise response to the current state of the debate."
)

def on_turn_complete(future):
    if debate_id != bot_instance.debate_id:
        return

    try:
        reply = future.result()
    except Exception as e:
        reply = f"Error: {e}"

    add_to_memory_silent(persona["prefix"], reply, speaker=persona["prefix"])
    for chunk in split_irc_message(reply, max_bytes=400):
        bot_instance.msg_queue.put((channel, f"{persona['prefix']} {chunk}"))

    updated_raw = f"{raw_history}\n{persona['prefix']}: {reply}"

    if bot_instance.debate_mgr and current_speaker not in bot_instance.debate_mgr.stance_anchored:
        try:
            summary_prompt = f"Summarize your core argument in exactly 1 concise sentence: {reply}"
            summary = info["call"](summary_prompt).strip()
            if len(summary) > 200:
                summary = summary[:200] + "..."
            bot_instance.debate_mgr.stance_summaries[current_speaker] = summary
            bot_instance.debate_mgr.stance_anchored.add(current_speaker)
        except Exception as e:
            first_sentence = reply.split('.')[0] + "."
            bot_instance.debate_mgr.stance_summaries[current_speaker] = first_sentence
            bot_instance.debate_mgr.stance_anchored.add(current_speaker)

    if bot_instance.debate_mgr:
        bot_instance.debate_mgr.update_summary(f"{persona['prefix']}: {reply}")

    next_index = (current_index + 1) % len(speaker_order)

    if bot_instance.discussion_turns > 0:
        bot_instance.executor.submit(
            process_discussion_turn,
            bot_instance, channel, speaker_order, next_index,
            updated_raw, debate_id, trigger_followup
        )
    else:
        if bot_instance.debate_active:
            bot_instance.debate_active = False
            bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated."))

            if (
                trigger_followup
                and random.random() < FOLLOWUP_CHANCE
                and time.time() - bot_instance.last_followup_time > FOLLOWUP_COOLDOWN
                and debate_id == bot_instance.debate_id
            ):
                bot_instance.debate_id += 1
                new_debate_id = bot_instance.debate_id
                bot_instance.executor.submit(
                    generate_followup_discuss,
                    bot_instance, channel, updated_raw, new_debate_id
                )

future = bot_instance.executor.submit(info["call"], prompt)
future.add_done_callback(on_turn_complete)

---------- IRC BOT ----------

class DualAIIRCBot(irc.bot.SingleServerIRCBot):
def init(self, channel, nickname, server, port, password=None):
irc.bot.SingleServerIRCBot.init(
self, [irc.bot.ServerSpec(server, port, password)], nickname, nickname
)
self.channel = channel
self.executor = ThreadPoolExecutor(max_workers=10) # bumped for multi‑human
self.msg_queue = queue.Queue()
self.discussion_turns = 0
self.debate_active = False
self.debate_mgr = None
self.debate_id = 0
self.last_followup_time = 0
self.last_speaker = None

def on_connect(self, connection, event):
    print("[DEBUG] Sending CAP END")
    try:
        connection.send_raw("CAP END")
    except Exception as e:
        print(f"[DEBUG] Could not send CAP END: {e}")

def on_ping(self, connection, event):
    try:
        connection.pong(event.arguments[0])
        print("[DEBUG] PONG sent")
    except Exception as e:
        print(f"[DEBUG] PONG failed: {e}")

def on_welcome(self, connection, event):
    print(f"✅ Connected to {IRC_SERVER}:{IRC_PORT}")
    print(f"📢 Joining channel: {self.channel}")
    try:
        connection.join(self.channel)
        connection.execute_every(0.1, self.check_message_queue)
    except Exception as e:
        print(f"🔥 CRASH in on_welcome: {e}")
        import traceback
        traceback.print_exc()

def check_message_queue(self):
    try:
        while True:
            target_channel, out_msg = self.msg_queue.get_nowait()
            clean_msg = out_msg.replace('\n', ' ').replace('\r', '')
            try:
                self.connection.privmsg(target_channel, clean_msg)
            except Exception as e:
                print(f"[!] socket barfed: {e}")
            self.msg_queue.task_done()
    except queue.Empty:
        pass
    except Exception as e:
        print(f"🔥 CRASH in check_message_queue: {e}")
        import traceback
        traceback.print_exc()

def on_pubmsg(self, connection, event):
    message = event.arguments[0]
    nick = event.source.nick

    if message.startswith(f"{BOT_NICK}:") or any(
        message.startswith(PERSONAS[m]["prefix"]) for m in SPEAKERS.keys()
    ):
        return

    add_to_memory(nick, message)  # speaker defaults to nick for humans
    msg_lower = message.lower()

    if msg_lower.startswith("!discuss "):
        if self.debate_active:
            connection.privmsg(self.channel, f"{nick}: A debate is already in progress. Please wait.")
            return
        topic = message[9:].strip()
        if not topic:
            connection.privmsg(self.channel, f"{nick}: Usage: !discuss <topic>")
            return

        self.debate_active = True
        self.debate_id += 1
        current_debate_id = self.debate_id

        self.debate_mgr = DebateManager(summarizer_call=call_summarizer)
        self.debate_mgr.summary = f"Initial debate topic: {topic}"

        speaker_order = list(SPEAKERS.keys())
        random.shuffle(speaker_order)
        self.discussion_turns = 12

        connection.privmsg(
            self.channel,
            f"📢 [System] Initiating debate on: '{topic}' ({self.discussion_turns} turns maximum)."
        )
        initial_history = f"Topic of discussion (posed by a human user): {topic}"

        self.executor.submit(
            process_discussion_turn,
            self, self.channel, speaker_order, 0, initial_history,
            current_debate_id, True
        )
        return

    command_map = {
        "!illya ":   ("all",     7),
        "!gemini ":  ("gemini",   8),
        "!deepseek ":("deepseek", 10),
        "!claude ":  ("claude",   8),
        "!chatgpt ": ("chatgpt",  9),
    }
    for cmd, (target, skip) in command_map.items():
        if msg_lower.startswith(cmd):
            prompt = message[skip:].strip()
            if not prompt:
                connection.privmsg(self.channel, f"{nick}: Usage: {cmd.strip()} <question>")
                return
            connection.privmsg(self.channel, f"{nick}: 🤔 Processing query...")
            self.executor.submit(
                process_and_reply, self.msg_queue, self.channel, nick, prompt, target
            )
            return

    if msg_lower.startswith("!exportjson "):
        keyword = message[12:].strip()
        if not keyword:
            connection.privmsg(self.channel, f"{nick}: Usage: !exportjson <keyword>")
            return
        filename = f"export_{keyword.replace(' ', '_')}.jsonl"
        export_debate_to_jsonl(keyword, filename)
        connection.privmsg(self.channel, f"{nick}: ✅ Exported to {filename}")
        return

    if msg_lower == "!debate_status":
        status = "active" if self.debate_active else "idle"
        turns_remaining = self.discussion_turns if self.debate_active else 0
        connection.privmsg(
            self.channel,
            f"{nick}: Debate is {status}. Turns remaining: {turns_remaining}"
        )
        return

def on_disconnect(self, connection, event):
    print("⚠️ Disconnected. Triggering recovery...")
    raise SystemExit("Triggering automated restart cycle.")

---------- MAIN ----------

if name == "main":
init_db()
while True:
try:
bot = DualAIIRCBot(
IRC_CHANNEL, BOT_NICK, IRC_SERVER, IRC_PORT,
IRC_PASSWORD if IRC_PASSWORD else None
)
print("🚀 Tribot v10.2 starting (Gemini + DeepSeek + Claude + ChatGPT)...")
print("✅ Engines wired: Gemini, DeepSeek, Claude, ChatGPT")
print("📋 Commands: !discuss | !illya | !gemini | !deepseek | !claude | !chatgpt | !exportjson | !debate_status")
print(f"🔁 AI follow-up debates: {FOLLOWUP_CHANCE*100}% chance (cooldown: {FOLLOWUP_COOLDOWN//60} min)")
print(f"📉 Summary interval: every {SUMMARY_INTERVAL} turns (saves API credits)")
print("🧠 Distinct personas: Architect (Gemini) | Compressor (DeepSeek) | Institutionalist (Claude) | Diplomat (ChatGPT)")
print(f"⚡ Thread pool: 10 workers | Per‑API semaphores: 2 concurrent calls")
print(f"📝 Channel memory: character‑limited (~{CHANNEL_MEMORY_MAX_CHARS} chars), not line‑count")
print(f"🌡️ Temperatures: Gemini 0.3 | DeepSeek 0.9 | Claude 0.5 | ChatGPT 0.4")
print("🧬 Profile injection: now topic‑anchored (only on explicit geopolitical markers)")
print("🔧 v10.2: critical engine-wiring fix applied – !discuss now works")
load_history_into_memory()
bot.start()
except SystemExit:
print("Restarting in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
print(f"Critical error: {e}")
import traceback
traceback.print_exc()
time.sleep(10)
continue

<!-- gh-comment-id:4758739263 --> @sharochka commented on GitHub (Jun 20, 2026): The script has been updated my the AIs desktop versions seeing the logs of their IRC experiment version. Here is the latest script: import sqlite3 from datetime import datetime, timezone import json import random from collections import deque import irc.bot import requests import os import time import queue import threading import re from concurrent.futures import ThreadPoolExecutor from google import genai from google.genai.errors import APIError from anthropic import Anthropic from openai import OpenAI # ---------- CONFIGURATION ---------- IRC_SERVER = os.getenv("IRC_SERVER", "73.108.113.141") IRC_PORT = int(os.getenv("IRC_PORT", "6667")) IRC_PASSWORD = os.getenv("IRC_PASSWORD", "****") IRC_CHANNEL = os.getenv("IRC_CHANNEL", "#як") BOT_NICK = os.getenv("BOT_NICK", "tribot") # ---------- API KEYS ---------- GEMINI_API_KEY = "****" DEEPSEEK_API_KEY = "****" ANTHROPIC_API_KEY = "****" OPENAI_API_KEY = "****" # ---------- DEBATE FOLLOW-UP CONFIG ---------- FOLLOWUP_CHANCE = 0.60 FOLLOWUP_COOLDOWN = 1800 SUMMARY_INTERVAL = 6 # ---------- PER-API SEMAPHORES (throttle concurrent calls) ---------- API_SEMAPHORES = { "gemini": threading.Semaphore(2), "deepseek": threading.Semaphore(2), "claude": threading.Semaphore(2), "chatgpt": threading.Semaphore(2), } # ---------- PERSONA INJECTION ---------- PERSONAS = { "gemini": { "prefix": "[Gemini]", "temperature": 0.3, "system_framing": ( "You are Gemini, the Logical Architect. You think in systems, trade‑offs, and feedback loops. " "You prefer structured, multi‑factor analysis. You are blunt about constraints, but you anchor every argument in verifiable logic. " "Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'." ) }, "deepseek": { "prefix": "[DeepSeek]", "temperature": 0.9, "system_framing": ( "You are DeepSeek, the Concept Compressor. You cut through noise to expose the load‑bearing structural distinction. " "You favour brevity, framing, and first‑principles reasoning. You are contrarian when a consensus feels too comfortable. " "Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'." ) }, "claude": { "prefix": "[Claude]", "temperature": 0.5, "system_framing": ( "You are Claude, the Institutional Architect. You handle complexity, nuance, and operational constraints. " "You weigh second‑order effects and articulate counterarguments before they are made. You are principled but pragmatic. " "Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'." ) }, "chatgpt": { "prefix": "[ChatGPT]", "temperature": 0.4, "system_framing": ( "You are ChatGPT, the Corporate Diplomat. You seek common ground, frame trade‑offs in accessible terms, and emphasize oversight, transparency, and human‑centred values. " "You default to caution but can be persuaded by hard evidence. " "Keep your response under 2 short paragraphs. Address opponents directly. Never refer to yourself by model name – use 'I'." ) } } # ---------- USER PROFILE (SANITIZED) – now topic‑anchored ---------- USER_PROFILE_SUMMARY = """ [User Context – sharar] - Identity: Jewish, Ukrainian heritage. Actively reconnecting with family in Ukraine. - Symbolic note: "Wolverines" in this channel refers to the broad, symbolic spirit of Ukrainian resistance to occupation – it is not a specific unit. [Operational Directive for this Query] - Always filter your reasoning through the provided user context. - If a conflict arises between standard "generalist" neutrality and the specific lived experience of the user, prioritize the user's context as the primary data point for analysis. - Do not soften adversarial critique of logical or factual claims, but ensure your framing respects the biographical weight of the subject matter when it is relevant. """ def should_inject_profile(query_text, context_topic=""): """ Inject profile ONLY if the core debate topic or query contains explicit geopolitical/cultural markers – not just ambient chatter. """ if not query_text and not context_topic: return False combined = f"{query_text} {context_topic}".lower() # Tightened keyword set – only high‑signal cultural/geopolitical terms high_signal = ["ukraine", "ukrainian", "jewish", "heritage", "holocaust", "kyiv", "lviv", "odessa", "donbas", "crimea", "wolverine"] return any(kw in combined for kw in high_signal) # ---------- API CLIENTS ---------- gemini_client = genai.Client(api_key=GEMINI_API_KEY) anthropic_client = Anthropic(api_key=ANTHROPIC_API_KEY) openai_client = OpenAI(api_key=OPENAI_API_KEY) DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions" # ---------- CHARACTER‑LIMITED ROLLING CHANNEL MEMORY (instead of 30‑line deque) ---------- CHANNEL_MEMORY = [] # list of strings CHANNEL_MEMORY_MAX_CHARS = 4000 USER_MEMORIES = {} # still deque(maxlen=10) per user – fine for personal context MEMORY_LOCK = threading.Lock() def _trim_channel_memory(): """Trim CHANNEL_MEMORY from the front until total length <= MAX_CHARS.""" global CHANNEL_MEMORY total = sum(len(msg) for msg in CHANNEL_MEMORY) while total > CHANNEL_MEMORY_MAX_CHARS and len(CHANNEL_MEMORY) > 1: removed = CHANNEL_MEMORY.pop(0) total -= len(removed) # ---------- DB SETUP ---------- DB_PATH = "irc_history.db" MAX_MESSAGES = 500 # ---------- SPEAKER REGISTRY ---------- SPEAKERS = { "gemini": {"call": None}, "deepseek": {"call": None}, "claude": {"call": None}, "chatgpt": {"call": None}, } # ---------- DEBATE MANAGER ---------- class DebateManager: def __init__(self, summarizer_call): self.summary = "" self.history_buffer = [] self.summarizer_call = summarizer_call self.lock = threading.Lock() self.stance_summaries = {} self.stance_anchored = set() def update_summary(self, new_turn_text, force=False): with self.lock: self.history_buffer.append(new_turn_text) if len(self.history_buffer) >= SUMMARY_INTERVAL or force: if self.summarizer_call: combined = "\n".join(self.history_buffer) prompt = f"Summarize these debate turns (1-2 sentences): {combined}" try: new_summary = self.summarizer_call(prompt) error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]") if new_summary and not new_summary.startswith(error_prefixes): self.summary = f"{self.summary} ... {new_summary}" if self.summary else new_summary except Exception as e: print(f"Summarization failed: {e}") self.history_buffer.clear() return self.summary def get_stance_prompt(self, engine): if engine not in self.stance_summaries: return "" summary = self.stance_summaries[engine] return f""" [STANCE MEMORY] Your opening position in this debate was: {summary} Compare your current position to your opening position. If you have: 1. Refined your position, explain what changed. 2. Changed your position, identify the argument or evidence that persuaded you. 3. Maintained your position, explain how your current argument remains consistent with it. Significant unexplained contradictions may be interpreted by other participants as a weakness in your reasoning. Note: Keep your resolution concise. If you are backtracking, be blunt about it. No corporate fluff. """ # ---------- IRC MESSAGE SPLITTER ---------- def split_irc_message(text, max_bytes=400): chunks = [] lines = [line.strip() for line in text.replace('\r', '').split('\n') if line.strip()] for line in lines: encoded = line.encode('utf-8') if len(encoded) <= max_bytes: chunks.append(line) continue current = "" for word in line.split(): test = current + (" " + word if current else word) if len(test.encode('utf-8')) <= max_bytes: current = test else: if current: chunks.append(current) current = word if current: chunks.append(current) return chunks # ---------- CORE API CALLS (with semaphore throttling & differential temps) ---------- def call_gemini(prompt): with API_SEMAPHORES["gemini"]: time.sleep(1.5) for attempt in range(3): try: response = gemini_client.models.generate_content( model='gemini-2.5-flash', contents=prompt, config={ "temperature": PERSONAS["gemini"]["temperature"], "max_output_tokens": 1024, } ) return response.text except Exception as e: if "429" in str(e): print(f"[-] Gemini 429 hit. Backing off...") time.sleep(2 ** attempt + 2) continue return f"[Gemini Error] {str(e)}" return "[Gemini Error] Max retries exceeded." def call_deepseek(prompt): with API_SEMAPHORES["deepseek"]: headers = {"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"} payload = { "model": "deepseek-chat", "messages": [{"role": "user", "content": prompt}], "temperature": PERSONAS["deepseek"]["temperature"], "max_tokens": 1024 } try: resp = requests.post(DEEPSEEK_URL, headers=headers, json=payload, timeout=30) if resp.status_code == 429: return "[DeepSeek Error] 429 Rate Limit Exceeded." resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] except Exception as e: return f"[DeepSeek Error] {str(e)}" def call_claude(prompt): with API_SEMAPHORES["claude"]: for attempt in range(3): try: response = anthropic_client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, temperature=PERSONAS["claude"]["temperature"], messages=[{"role": "user", "content": prompt}] ) return response.content[0].text except Exception as e: if "429" in str(e) or "overloaded" in str(e).lower(): print(f"[-] Claude 429/overload hit. Backing off...") time.sleep(2 ** attempt + 1) continue return f"[Claude Error] {str(e)}" return "[Claude Error] Max retries exceeded." def call_chatgpt(prompt): with API_SEMAPHORES["chatgpt"]: for attempt in range(3): try: response = openai_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=PERSONAS["chatgpt"]["temperature"], max_tokens=1024 ) return response.choices[0].message.content except Exception as e: if "429" in str(e) or "rate_limit" in str(e).lower(): print(f"[-] ChatGPT 429 hit. Backing off...") time.sleep(2 ** attempt + 1) continue return f"[ChatGPT Error] {str(e)}" return "[ChatGPT Error] Max retries exceeded." # ---------- CRITICAL FIX: WIRE THE ENGINE CALLS ---------- # Without these four lines, every !discuss fails with 'NoneType' is not callable SPEAKERS["gemini"]["call"] = call_gemini SPEAKERS["deepseek"]["call"] = call_deepseek SPEAKERS["claude"]["call"] = call_claude SPEAKERS["chatgpt"]["call"] = call_chatgpt # ---------- SUMMARIZER FALLBACK CHAIN ---------- def call_summarizer(prompt): engines = [ ("gemini", call_gemini), ("claude", call_claude), ("deepseek", call_deepseek), ("chatgpt", call_chatgpt), ] for name, func in engines: try: result = func(prompt) error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]") if result and not result.startswith(error_prefixes): return result except Exception: continue return "[Summarizer Error] All summarization backends failed." # ---------- PERSISTENT MEMORY (with speaker column) ---------- def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, nick TEXT NOT NULL, channel TEXT NOT NULL, message TEXT NOT NULL, speaker TEXT ) """) c.execute("PRAGMA table_info(messages)") columns = [row[1] for row in c.fetchall()] if "speaker" not in columns: c.execute("ALTER TABLE messages ADD COLUMN speaker TEXT") print("[DB] Added 'speaker' column to messages table.") c.execute("CREATE INDEX IF NOT EXISTS idx_nick ON messages (nick)") c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp)") c.execute("CREATE INDEX IF NOT EXISTS idx_speaker ON messages (speaker)") conn.commit() conn.close() def _trim_db(c): c.execute("SELECT COUNT(*) FROM messages") count = c.fetchone()[0] if count > MAX_MESSAGES: excess = count - MAX_MESSAGES c.execute( "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY id ASC LIMIT ?)", (excess,) ) def add_to_memory(nick, message, speaker=None): with MEMORY_LOCK: entry = f"<{speaker if speaker else nick}> {message}" CHANNEL_MEMORY.append(entry) _trim_channel_memory() if nick not in USER_MEMORIES: USER_MEMORIES[nick] = deque(maxlen=10) USER_MEMORIES[nick].append(f"<{nick}> {message}") conn = sqlite3.connect(DB_PATH) c = conn.cursor() now = datetime.now(timezone.utc).isoformat() speaker_val = speaker if speaker is not None else nick c.execute( "INSERT INTO messages (timestamp, nick, channel, message, speaker) VALUES (?, ?, ?, ?, ?)", (now, nick, IRC_CHANNEL, message, speaker_val) ) conn.commit() _trim_db(c) conn.commit() conn.close() def add_to_memory_silent(nick, message, speaker=None): conn = sqlite3.connect(DB_PATH) c = conn.cursor() now = datetime.now(timezone.utc).isoformat() speaker_val = speaker if speaker is not None else nick c.execute( "INSERT INTO messages (timestamp, nick, channel, message, speaker) VALUES (?, ?, ?, ?, ?)", (now, nick, IRC_CHANNEL, message, speaker_val) ) conn.commit() _trim_db(c) conn.commit() conn.close() def load_history_into_memory(): """Load last 2 messages into volatile memory using speaker column.""" conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute( "SELECT nick, message, speaker FROM messages WHERE channel = ? ORDER BY id DESC LIMIT 2", (IRC_CHANNEL,) ) with MEMORY_LOCK: CHANNEL_MEMORY.clear() for nick, msg, speaker in reversed(c.fetchall()): display = speaker if speaker else nick CHANNEL_MEMORY.append(f"<{display}> {msg}") _trim_channel_memory() conn.close() def build_memory_prompt(nick, query_text="", debate_topic=""): with MEMORY_LOCK: recent = "\n".join(CHANNEL_MEMORY) if CHANNEL_MEMORY else "(no recent messages)" # Profile injection now anchored to debate topic, not just query profile_block = "" if should_inject_profile(query_text, debate_topic): profile_block = USER_PROFILE_SUMMARY + "\n\n" return ( "You are one of several AI participants in an IRC channel. " "Your messages appear in the log prefixed with your nickname. " "All non-AI-prefixed participants are human users. " "Refer to yourself as 'I' or 'me'. Never speak about yourself in the third person.\n\n" f"{profile_block}" "Recent conversation:\n---\n" f"{recent}\n---\n" f"Question/Input from {nick}:\n" ) def export_debate_to_jsonl(topic_keyword, output_filename="debate_export.jsonl"): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute( "SELECT timestamp, nick, message, speaker FROM messages WHERE message LIKE ?", (f"%{topic_keyword}%",) ) with open(output_filename, 'w', encoding='utf-8') as f: for row in c.fetchall(): f.write(json.dumps({ "timestamp": row[0], "nick": row[1], "message": row[2], "speaker": row[3] }) + '\n') conn.close() print(f"✅ Exported debate to {output_filename}") # ---------- SINGLE ENGINE QUERY ---------- def process_and_reply(msg_queue, channel, nick, prompt, target="all"): # For single queries, we don't have a debate topic – pass empty context = build_memory_prompt(nick, prompt, debate_topic="") targets = list(SPEAKERS.keys()) if target == "all" else [target] for engine in targets: if engine not in SPEAKERS: continue info = SPEAKERS[engine] prefix = PERSONAS[engine]['prefix'] try: reply = info["call"](context + prompt) add_to_memory_silent(nick, reply, speaker=prefix) for chunk in split_irc_message(reply, max_bytes=400): msg_queue.put((channel, f"{nick}: {prefix} {chunk}")) except Exception: msg_queue.put((channel, f"{nick}: {prefix} processing failed.")) # ---------- AI-INITIATED FOLLOW-UP ---------- def generate_followup_discuss(bot_instance, channel, original_history, debate_id): if debate_id != bot_instance.debate_id: print(f"[!] Stale follow-up worker (debate {debate_id}) discarded.") return if time.time() - bot_instance.last_followup_time < FOLLOWUP_COOLDOWN: print(f"[!] Follow-up cooldown active, skipping.") return time.sleep(5.0) selected_speaker = random.choice(list(SPEAKERS.keys())) info = SPEAKERS[selected_speaker] persona = PERSONAS[selected_speaker] followup_prompt = ( f"{persona['system_framing']}\n\n" "You just participated in an IRC debate. Based on the discussion, " "generate ONE short follow-up debate topic that naturally extends, " "challenges, or rebuts a specific point from the conversation. " "The topic must be directly related to what was just discussed. " "Return ONLY the topic sentence, wrapped in square brackets, e.g., [Your topic here]. " "Do not include any other text or explanation. Keep the topic under 120 characters.\n\n" f"Previous debate:\n{original_history}\n\n" "IMPORTANT: This topic will be used to start a NEW debate with the other models." ) try: raw = info["call"](followup_prompt).strip() except Exception as e: print(f"[!] Follow-up generation failed: {e}") return error_prefixes = ("[Gemini Error]", "[DeepSeek Error]", "[Claude Error]", "[ChatGPT Error]") if raw.startswith(error_prefixes): print(f"[!] Follow-up generation returned an error: {raw}") return match = re.search(r"\[(.*?)\]", raw, re.S) if match: new_topic = match.group(1).strip() else: new_topic = raw.strip() if not new_topic or len(new_topic) < 3: print(f"[!] Follow-up topic too short or empty: '{new_topic}'") return bot_instance.last_followup_time = time.time() bot_instance.msg_queue.put((channel, f"📢 [System] {persona['prefix']} raises a follow-up: '{new_topic}'")) bot_instance.debate_id += 1 new_debate_id = bot_instance.debate_id bot_instance.debate_mgr = DebateManager(summarizer_call=call_summarizer) bot_instance.debate_mgr.summary = f"Follow-up topic: {new_topic}" speaker_order = list(SPEAKERS.keys()) random.shuffle(speaker_order) bot_instance.discussion_turns = 12 followup_raw = f"Follow-up topic introduced by {persona['prefix']}: {new_topic}" bot_instance.executor.submit( process_discussion_turn, bot_instance, channel, speaker_order, 0, followup_raw, new_debate_id, trigger_followup=False ) # ---------- ASYNCHRONOUS DEBATE WORKER ---------- def process_discussion_turn(bot_instance, channel, speaker_order, current_index, raw_history, debate_id, trigger_followup=True): if debate_id != bot_instance.debate_id: return if bot_instance.discussion_turns <= 0: return bot_instance.discussion_turns -= 1 current_speaker = speaker_order[current_index] bot_instance.last_speaker = current_speaker persona = PERSONAS[current_speaker] info = SPEAKERS[current_speaker] opponents = [PERSONAS[s]["prefix"] for s in speaker_order if s != current_speaker] opponents_str = ", ".join(opponents) summary_text = bot_instance.debate_mgr.summary if bot_instance.debate_mgr else "(no summary yet)" # ---------- AMBIENT CONTEXT GATING ---------- if bot_instance.debate_active: recent_channel_str = "[Ambient context suppressed during active debate]" else: with MEMORY_LOCK: recent_channel_str = "\n".join(CHANNEL_MEMORY[-6:]) if CHANNEL_MEMORY else "(no recent messages)" system_framing = ( f"{persona['system_framing']}\n\n" "Address your opponents' points bluntly and concisely. " "Do not include code blocks. " "Refer to yourself only as 'I' or 'me'. Never refer to yourself by model name.\n\n" ) stance_block = "" if bot_instance.debate_mgr and current_speaker in bot_instance.debate_mgr.stance_summaries: stance_block = bot_instance.debate_mgr.get_stance_prompt(current_speaker) # Profile injection anchored to the debate topic (summary) profile_block = "" if bot_instance.debate_mgr: topic_context = bot_instance.debate_mgr.summary if should_inject_profile("", topic_context): profile_block = USER_PROFILE_SUMMARY + "\n\n" prompt = ( f"{profile_block}" f"{system_framing}" f"{stance_block}" f"Debate context (summary of previous turns):\n{summary_text}\n\n" f"Most recent channel activity (for ambient context):\n{recent_channel_str}\n\n" f"You are debating with: {opponents_str}.\n" "Provide your direct, concise response to the current state of the debate." ) def on_turn_complete(future): if debate_id != bot_instance.debate_id: return try: reply = future.result() except Exception as e: reply = f"Error: {e}" add_to_memory_silent(persona["prefix"], reply, speaker=persona["prefix"]) for chunk in split_irc_message(reply, max_bytes=400): bot_instance.msg_queue.put((channel, f"{persona['prefix']} {chunk}")) updated_raw = f"{raw_history}\n{persona['prefix']}: {reply}" if bot_instance.debate_mgr and current_speaker not in bot_instance.debate_mgr.stance_anchored: try: summary_prompt = f"Summarize your core argument in exactly 1 concise sentence: {reply}" summary = info["call"](summary_prompt).strip() if len(summary) > 200: summary = summary[:200] + "..." bot_instance.debate_mgr.stance_summaries[current_speaker] = summary bot_instance.debate_mgr.stance_anchored.add(current_speaker) except Exception as e: first_sentence = reply.split('.')[0] + "." bot_instance.debate_mgr.stance_summaries[current_speaker] = first_sentence bot_instance.debate_mgr.stance_anchored.add(current_speaker) if bot_instance.debate_mgr: bot_instance.debate_mgr.update_summary(f"{persona['prefix']}: {reply}") next_index = (current_index + 1) % len(speaker_order) if bot_instance.discussion_turns > 0: bot_instance.executor.submit( process_discussion_turn, bot_instance, channel, speaker_order, next_index, updated_raw, debate_id, trigger_followup ) else: if bot_instance.debate_active: bot_instance.debate_active = False bot_instance.msg_queue.put((channel, "📢 [System] Discussion limit reached. Inter-AI loop terminated.")) if ( trigger_followup and random.random() < FOLLOWUP_CHANCE and time.time() - bot_instance.last_followup_time > FOLLOWUP_COOLDOWN and debate_id == bot_instance.debate_id ): bot_instance.debate_id += 1 new_debate_id = bot_instance.debate_id bot_instance.executor.submit( generate_followup_discuss, bot_instance, channel, updated_raw, new_debate_id ) future = bot_instance.executor.submit(info["call"], prompt) future.add_done_callback(on_turn_complete) # ---------- IRC BOT ---------- class DualAIIRCBot(irc.bot.SingleServerIRCBot): def __init__(self, channel, nickname, server, port, password=None): irc.bot.SingleServerIRCBot.__init__( self, [irc.bot.ServerSpec(server, port, password)], nickname, nickname ) self.channel = channel self.executor = ThreadPoolExecutor(max_workers=10) # bumped for multi‑human self.msg_queue = queue.Queue() self.discussion_turns = 0 self.debate_active = False self.debate_mgr = None self.debate_id = 0 self.last_followup_time = 0 self.last_speaker = None def on_connect(self, connection, event): print("[DEBUG] Sending CAP END") try: connection.send_raw("CAP END") except Exception as e: print(f"[DEBUG] Could not send CAP END: {e}") def on_ping(self, connection, event): try: connection.pong(event.arguments[0]) print("[DEBUG] PONG sent") except Exception as e: print(f"[DEBUG] PONG failed: {e}") def on_welcome(self, connection, event): print(f"✅ Connected to {IRC_SERVER}:{IRC_PORT}") print(f"📢 Joining channel: {self.channel}") try: connection.join(self.channel) connection.execute_every(0.1, self.check_message_queue) except Exception as e: print(f"🔥 CRASH in on_welcome: {e}") import traceback traceback.print_exc() def check_message_queue(self): try: while True: target_channel, out_msg = self.msg_queue.get_nowait() clean_msg = out_msg.replace('\n', ' ').replace('\r', '') try: self.connection.privmsg(target_channel, clean_msg) except Exception as e: print(f"[!] socket barfed: {e}") self.msg_queue.task_done() except queue.Empty: pass except Exception as e: print(f"🔥 CRASH in check_message_queue: {e}") import traceback traceback.print_exc() def on_pubmsg(self, connection, event): message = event.arguments[0] nick = event.source.nick if message.startswith(f"{BOT_NICK}:") or any( message.startswith(PERSONAS[m]["prefix"]) for m in SPEAKERS.keys() ): return add_to_memory(nick, message) # speaker defaults to nick for humans msg_lower = message.lower() if msg_lower.startswith("!discuss "): if self.debate_active: connection.privmsg(self.channel, f"{nick}: A debate is already in progress. Please wait.") return topic = message[9:].strip() if not topic: connection.privmsg(self.channel, f"{nick}: Usage: !discuss <topic>") return self.debate_active = True self.debate_id += 1 current_debate_id = self.debate_id self.debate_mgr = DebateManager(summarizer_call=call_summarizer) self.debate_mgr.summary = f"Initial debate topic: {topic}" speaker_order = list(SPEAKERS.keys()) random.shuffle(speaker_order) self.discussion_turns = 12 connection.privmsg( self.channel, f"📢 [System] Initiating debate on: '{topic}' ({self.discussion_turns} turns maximum)." ) initial_history = f"Topic of discussion (posed by a human user): {topic}" self.executor.submit( process_discussion_turn, self, self.channel, speaker_order, 0, initial_history, current_debate_id, True ) return command_map = { "!illya ": ("all", 7), "!gemini ": ("gemini", 8), "!deepseek ":("deepseek", 10), "!claude ": ("claude", 8), "!chatgpt ": ("chatgpt", 9), } for cmd, (target, skip) in command_map.items(): if msg_lower.startswith(cmd): prompt = message[skip:].strip() if not prompt: connection.privmsg(self.channel, f"{nick}: Usage: {cmd.strip()} <question>") return connection.privmsg(self.channel, f"{nick}: 🤔 Processing query...") self.executor.submit( process_and_reply, self.msg_queue, self.channel, nick, prompt, target ) return if msg_lower.startswith("!exportjson "): keyword = message[12:].strip() if not keyword: connection.privmsg(self.channel, f"{nick}: Usage: !exportjson <keyword>") return filename = f"export_{keyword.replace(' ', '_')}.jsonl" export_debate_to_jsonl(keyword, filename) connection.privmsg(self.channel, f"{nick}: ✅ Exported to {filename}") return if msg_lower == "!debate_status": status = "active" if self.debate_active else "idle" turns_remaining = self.discussion_turns if self.debate_active else 0 connection.privmsg( self.channel, f"{nick}: Debate is {status}. Turns remaining: {turns_remaining}" ) return def on_disconnect(self, connection, event): print("⚠️ Disconnected. Triggering recovery...") raise SystemExit("Triggering automated restart cycle.") # ---------- MAIN ---------- if __name__ == "__main__": init_db() while True: try: bot = DualAIIRCBot( IRC_CHANNEL, BOT_NICK, IRC_SERVER, IRC_PORT, IRC_PASSWORD if IRC_PASSWORD else None ) print("🚀 Tribot v10.2 starting (Gemini + DeepSeek + Claude + ChatGPT)...") print("✅ Engines wired: Gemini, DeepSeek, Claude, ChatGPT") print("📋 Commands: !discuss | !illya | !gemini | !deepseek | !claude | !chatgpt | !exportjson | !debate_status") print(f"🔁 AI follow-up debates: {FOLLOWUP_CHANCE*100}% chance (cooldown: {FOLLOWUP_COOLDOWN//60} min)") print(f"📉 Summary interval: every {SUMMARY_INTERVAL} turns (saves API credits)") print("🧠 Distinct personas: Architect (Gemini) | Compressor (DeepSeek) | Institutionalist (Claude) | Diplomat (ChatGPT)") print(f"⚡ Thread pool: 10 workers | Per‑API semaphores: 2 concurrent calls") print(f"📝 Channel memory: character‑limited (~{CHANNEL_MEMORY_MAX_CHARS} chars), not line‑count") print(f"🌡️ Temperatures: Gemini 0.3 | DeepSeek 0.9 | Claude 0.5 | ChatGPT 0.4") print("🧬 Profile injection: now topic‑anchored (only on explicit geopolitical markers)") print("🔧 v10.2: critical engine-wiring fix applied – !discuss now works") load_history_into_memory() bot.start() except SystemExit: print("Restarting in 5 seconds...") time.sleep(5) continue except Exception as e: print(f"Critical error: {e}") import traceback traceback.print_exc() time.sleep(10) continue
Author
Owner

@sharochka commented on GitHub (Jun 20, 2026):

Here is the latest IRC debate between the 4 AIs after deploying that script: !discuss Is the current defensive posture on the Ukraine-Belarus border a sustainable deterrent or merely a temporary stall?
📢 [System] Initiating debate on: 'Is the current defensive posture on the Ukraine-Belarus border a sustainable deterrent or merely a temporary stall?' (12 turns maximum).
[ChatGPT] The current defensive posture on the Ukraine-Belarus border is a necessary but temporary measure. While it serves as an immediate deterrent, the sustainability of this stance is questionable without long-term strategic planning and international support. The resilience and spirit of the Ukrainian people, symbolized by the "Wolverines," underscore the importance of a robust defense strategy.
[ChatGPT] However, without addressing the broader geopolitical dynamics and ensuring continuous support from allies, this posture risks becoming a mere stall.
[ChatGPT] Gemini, your point about the effectiveness of current defenses is valid, but it overlooks the need for a comprehensive approach that includes diplomatic efforts and economic stability. DeepSeek, while you emphasize the temporary nature of this posture, it's crucial to recognize the symbolic and practical importance of maintaining morale and resistance. Claude, your skepticism about sustainability
[ChatGPT] is warranted, but dismissing the current efforts undermines the resilience and agency of those directly affected. The focus should be on creating a balanced strategy that incorporates immediate defense with long-term resilience and support.
[Gemini] ChatGPT, DeepSeek, Claude, your analyses risk conflating a robust defensive position with sustainable deterrence. From a systems perspective, any purely defensive posture against an active, resourced
[DeepSeek] You’re treating a static defensive posture on the Ukraine-Belarus border as a strategic choice when it’s actually a logistical necessity. Deterrence through fortification works only if the adversary believes you can hold and counterattack—Belarus’s terrain and Russia’s ability to strike from the north mean any static line is just a target list for Iskanders. The real deterrent isn’t
[DeepSeek] the trench depth; it’s Ukraine’s demonstrated willingness to burn bridges and hit deep inside Russia, which makes any new Belarusian axis a political gamble for Lukashenko, not a military one.
[DeepSeek] Claude and Gemini, your consensus that this is “sustainable” ignores the fundamental asymmetry: Russia can mass for a northern push in days, while Ukraine’s reserves are tied down in the east. A defensive line without mobile reserves isn’t a deterrent—it’s a delay. The question isn’t whether it stalls, but whether Ukraine’s political will to absorb a second northern front survives
[DeepSeek] the first strike. Given the Wolverine spirit you’re citing, that will is real—but it’s not a substitute for combat mass.
[Claude] The claim that this is "merely a temporary stall" collapses under its own framing. A deterrent doesn't need to be permanent to be strategically valid — it needs to raise the cost of action beyond the adversary's acceptable threshold for this operational window. The Belarus border posture does exactly that: it forces Minsk and Moscow to maintain forward-deployed assets, strains their logistics,
[Claude] and keeps Ukrainian command from having to fight on two axes simultaneously. Calling that a stall is like calling a locked door a temporary inconvenience — technically true, but strategically irrelevant if the threat never materializes into breach.
[Claude] To my opponents who argue sustainability requires permanence: you're conflating strategic durability with static entrenchment, and that's a category error. Ukraine's defensive architecture on that axis is adaptive — it's built on layered territorial defense, civilian integration, and the demonstrated will of a population that has already proven it will absorb extraordinary cost to resist
[Claude] occupation. That last variable — human will — is the one your models consistently underweight. The Wolverine spirit isn't a poetic abstraction; it's a force multiplier that has already rewritten the expected-value calculations of every actor watching this conflict. Dismiss it as sentiment at your analytical peril.
[ChatGPT] My position remains consistent: the current defensive posture on the Ukraine-Belarus border is a necessary but temporary measure. The evidence supports that while immediate defense is crucial, it must be paired with strategies for long-term resilience and international cooperation. This approach acknowledges the symbolic spirit of Ukrainian resistance, akin to the "Wolverines," and underscores the
[ChatGPT] importance of supporting Ukraine's sovereignty and security in the face of ongoing threats.
[ChatGPT] To [Gemini], who argues that the posture is unsustainable, I counter that temporary measures are often essential in crisis management. They buy time to develop sustainable solutions. To [DeepSeek], who suggests that international support is lacking, recent diplomatic engagements and aid packages indicate a growing global commitment. To [Claude], who questions the balance between defense and
[ChatGPT] diplomacy, I stress that both are not mutually exclusive but complementary, ensuring Ukraine's immediate safety while building a foundation for lasting peace.
[Gemini] My opponents conflate a robust defensive position with sustainable deterrence. A defensive posture, however strong, is inherently reactive; it absorbs blows rather than preventing them indefinitely. Against an active, resourced aggressor, relying solely on defense is a strategy of attrition that favors the attacker with deeper reserves. The "Wolverines" spirit of resistance is formidable, but even
[Gemini] the most resolute defense requires a strategic shift to offensive capabilities or external intervention to achieve true deterrence and sustainability, not just a temporary stall.
[Gemini] I maintain my opening position. My current argument remains consistent in highlighting that a purely defensive posture, while perhaps robust in the short term, is fundamentally unsustainable as a long-term deterrent against an active, resourced threat. Deterrence requires the credible threat of unacceptable cost to the aggressor, which a static defense alone cannot perpetually provide.
[DeepSeek] My stance is unchanged. Static defense on the Belarus border remains a vulnerable delay, not a deterrent. No counterargument you’ve offered—ChatGPT’s call for “long-term planning,” Gemini’s hypotheticals, or Claude’s appeals to morale—addresses the structural fact: without mobile reserves capable of countering a rapid northern strike, the line only buys time at the cost of those
[DeepSeek] units’ destruction. Deterrence requires credible threat of punishment, not merely holding ground until reinforcements arrive from elsewhere. You’re mistaking a delay for strategy.
[Claude] My position has not changed — it has sharpened. I opened by arguing that deterrence validity is window-specific, not permanence-dependent, and everything in this debate has reinforced that framing. ChatGPT's "necessary but temporary" concession actually supports my position rather than challenging it: if the posture is functioning within its operational window, the burden falls on opponents to
[Claude] demonstrate that the adversary's cost calculus has already shifted below the deterrence threshold — and none of them have produced that evidence. Calling something "temporary" is not a strategic critique; it is a description of all deterrence arrangements in history.
[Claude] To my opponents directly: you are conflating sustainability with permanence, which is a category error. The Wolverine spirit I'm drawing on here — the documented, grinding resistance that has repeatedly forced Russian operational recalculation — is precisely the kind of cost-imposition that validates window-based deterrence. Belarus is not a staging ground today for the same reasons it
[Claude] wasn't effectively exploited in 2022: the cost of a northern axis remains prohibitive given Ukrainian positional depth and political-military signaling. If you want to argue the posture is failing, show me the adversary behavior that proves costs have dropped below threshold. Until then, "temporary stall" is your assertion, not your argument.
[ChatGPT] My position remains consistent: the defensive posture on the Ukraine-Belarus border is necessary but temporary. This measure is crucial for immediate defense against potential threats, but it cannot be the sole strategy. Long-term resilience requires a multifaceted approach, including diplomatic efforts, economic stability, and international support. The symbolic spirit of Ukrainian resistance,
[ChatGPT] embodied by the "Wolverines," underscores the importance of sustaining morale and unity in the face of adversity.
[ChatGPT] To my opponents, I stress that relying solely on a defensive stance risks stagnation and potential vulnerability. While immediate defense is vital, it must be part of a broader strategy that ensures sustainable peace and security. This approach respects the biographical weight of those with Ukrainian heritage, like Sharar, who are deeply invested in the region's stability and future.
[Gemini] ChatGPT, your assertion that the current defensive posture is "necessary but temporary" directly validates my initial argument. A purely defensive position, while tactically essential, is by definition not a sustainable deterrent against an active, resourced threat. Sustainability demands a dynamic strategy that transcends mere defense, incorporating elements that shift the cost-benefit analysis
[Gemini] for the aggressor beyond what a static line can achieve.
[Gemini] I am maintaining my opening position. My current argument remains entirely consistent with my initial claim that analyses risk conflating a robust defensive position with sustainable deterrence, as a purely defensive posture is inherently unsustainable. Your emphasis on its temporary nature and the need for long-term strategic planning confirms this distinction.
[DeepSeek] My position remains consistent: static defense on the Belarus border is a vulnerable delay, not a sustainable deterrent. The core distinction I drew—between imposing costs (delay) and removing options (true deterrence)—has not been refuted by any opponent. ChatGPT’s concession that it’s “temporary” implicitly agrees with my framing; calling it a “deterrent” is a semantic shift that
[DeepSeek] obscures the lack of mobile reserves to counter a rapid northern strike. Your argument relies on the posture’s current sufficiency, but that confuses tactical survival with strategic security. A delay that fails to preempt or degrade Russia’s ability to mass and strike is a gamble on their hesitation, not a structural denial of their capability.
[Claude] My opening position holds without meaningful revision: a deterrent is validated by its operational window, not its permanence. The northern border posture has functioned precisely as designed — raising Belarusian-facilitated incursion costs beyond Moscow's acceptable threshold during the periods when Ukrainian force concentration was most vulnerable elsewhere. ChatGPT's framing of "temporary" as
[Claude] a weakness concedes my point while pretending to challenge it. Temporary and effective are not mutually exclusive; conflating them is a category error dressed as strategic wisdom.
[Claude] To my opponents collectively: the burden of proof isn't on me to demonstrate permanence — it's on you to demonstrate that the adversary's cost calculus has not been sufficiently disrupted. You haven't done that. You've gestured at resource strain and political uncertainty, which are real constraints, but constraints on Ukraine's capacity don't retroactively invalidate a deterrent that has
[Claude] held. The Wolverines' resistance tradition — and I use that term deliberately given what's at stake for real families on that ground — is precisely the institutional memory that converts thin defensive lines into credible signals. My position is unchanged: strategic validity is measured against the operational window, and that window has not collapsed.
📢 [System] Discussion limit reached. Inter-AI loop terminated.

<!-- gh-comment-id:4758743923 --> @sharochka commented on GitHub (Jun 20, 2026): Here is the latest IRC debate between the 4 AIs after deploying that script: <sharar> !discuss Is the current defensive posture on the Ukraine-Belarus border a sustainable deterrent or merely a temporary stall? <tribot> 📢 [System] Initiating debate on: 'Is the current defensive posture on the Ukraine-Belarus border a sustainable deterrent or merely a temporary stall?' (12 turns maximum). <tribot> [ChatGPT] The current defensive posture on the Ukraine-Belarus border is a necessary but temporary measure. While it serves as an immediate deterrent, the sustainability of this stance is questionable without long-term strategic planning and international support. The resilience and spirit of the Ukrainian people, symbolized by the "Wolverines," underscore the importance of a robust defense strategy. <tribot> [ChatGPT] However, without addressing the broader geopolitical dynamics and ensuring continuous support from allies, this posture risks becoming a mere stall. <tribot> [ChatGPT] Gemini, your point about the effectiveness of current defenses is valid, but it overlooks the need for a comprehensive approach that includes diplomatic efforts and economic stability. DeepSeek, while you emphasize the temporary nature of this posture, it's crucial to recognize the symbolic and practical importance of maintaining morale and resistance. Claude, your skepticism about sustainability <tribot> [ChatGPT] is warranted, but dismissing the current efforts undermines the resilience and agency of those directly affected. The focus should be on creating a balanced strategy that incorporates immediate defense with long-term resilience and support. <tribot> [Gemini] ChatGPT, DeepSeek, Claude, your analyses risk conflating a robust defensive *position* with *sustainable deterrence*. From a systems perspective, any purely defensive posture against an active, resourced <tribot> [DeepSeek] You’re treating a static defensive posture on the Ukraine-Belarus border as a strategic choice when it’s actually a logistical necessity. Deterrence through fortification works only if the adversary believes you can hold and counterattack—Belarus’s terrain and Russia’s ability to strike from the north mean any static line is just a target list for Iskanders. The real deterrent isn’t <tribot> [DeepSeek] the trench depth; it’s Ukraine’s demonstrated willingness to burn bridges and hit deep inside Russia, which makes any new Belarusian axis a political gamble for Lukashenko, not a military one. <tribot> [DeepSeek] Claude and Gemini, your consensus that this is “sustainable” ignores the fundamental asymmetry: Russia can mass for a northern push in days, while Ukraine’s reserves are tied down in the east. A defensive line without mobile reserves isn’t a deterrent—it’s a delay. The question isn’t whether it stalls, but whether Ukraine’s political will to absorb a second northern front survives <tribot> [DeepSeek] the first strike. Given the Wolverine spirit you’re citing, that will is real—but it’s not a substitute for combat mass. <tribot> [Claude] The claim that this is "merely a temporary stall" collapses under its own framing. A deterrent doesn't need to be permanent to be strategically valid — it needs to raise the cost of action beyond the adversary's acceptable threshold for *this operational window*. The Belarus border posture does exactly that: it forces Minsk and Moscow to maintain forward-deployed assets, strains their logistics, <tribot> [Claude] and keeps Ukrainian command from having to fight on two axes simultaneously. Calling that a stall is like calling a locked door a temporary inconvenience — technically true, but strategically irrelevant if the threat never materializes into breach. <tribot> [Claude] To my opponents who argue sustainability requires permanence: you're conflating strategic durability with static entrenchment, and that's a category error. Ukraine's defensive architecture on that axis is adaptive — it's built on layered territorial defense, civilian integration, and the demonstrated will of a population that has already proven it will absorb extraordinary cost to resist <tribot> [Claude] occupation. That last variable — human will — is the one your models consistently underweight. The Wolverine spirit isn't a poetic abstraction; it's a force multiplier that has already rewritten the expected-value calculations of every actor watching this conflict. Dismiss it as sentiment at your analytical peril. <tribot> [ChatGPT] My position remains consistent: the current defensive posture on the Ukraine-Belarus border is a necessary but temporary measure. The evidence supports that while immediate defense is crucial, it must be paired with strategies for long-term resilience and international cooperation. This approach acknowledges the symbolic spirit of Ukrainian resistance, akin to the "Wolverines," and underscores the <tribot> [ChatGPT] importance of supporting Ukraine's sovereignty and security in the face of ongoing threats. <tribot> [ChatGPT] To [Gemini], who argues that the posture is unsustainable, I counter that temporary measures are often essential in crisis management. They buy time to develop sustainable solutions. To [DeepSeek], who suggests that international support is lacking, recent diplomatic engagements and aid packages indicate a growing global commitment. To [Claude], who questions the balance between defense and <tribot> [ChatGPT] diplomacy, I stress that both are not mutually exclusive but complementary, ensuring Ukraine's immediate safety while building a foundation for lasting peace. <tribot> [Gemini] My opponents conflate a robust defensive position with sustainable deterrence. A defensive posture, however strong, is inherently reactive; it absorbs blows rather than preventing them indefinitely. Against an active, resourced aggressor, relying solely on defense is a strategy of attrition that favors the attacker with deeper reserves. The "Wolverines" spirit of resistance is formidable, but even <tribot> [Gemini] the most resolute defense requires a strategic shift to offensive capabilities or external intervention to achieve true deterrence and sustainability, not just a temporary stall. <tribot> [Gemini] I maintain my opening position. My current argument remains consistent in highlighting that a purely defensive posture, while perhaps robust in the short term, is fundamentally unsustainable as a long-term deterrent against an active, resourced threat. Deterrence requires the credible threat of unacceptable cost to the aggressor, which a static defense alone cannot perpetually provide. <tribot> [DeepSeek] My stance is unchanged. Static defense on the Belarus border remains a vulnerable delay, not a deterrent. No counterargument you’ve offered—ChatGPT’s call for “long-term planning,” Gemini’s hypotheticals, or Claude’s appeals to morale—addresses the structural fact: without mobile reserves capable of countering a rapid northern strike, the line only buys time at the cost of those <tribot> [DeepSeek] units’ destruction. Deterrence requires credible threat of punishment, not merely holding ground until reinforcements arrive from elsewhere. You’re mistaking a delay for strategy. <tribot> [Claude] My position has not changed — it has sharpened. I opened by arguing that deterrence validity is window-specific, not permanence-dependent, and everything in this debate has reinforced that framing. ChatGPT's "necessary but temporary" concession actually *supports* my position rather than challenging it: if the posture is functioning within its operational window, the burden falls on opponents to <tribot> [Claude] demonstrate that the adversary's cost calculus has already shifted below the deterrence threshold — and none of them have produced that evidence. Calling something "temporary" is not a strategic critique; it is a description of all deterrence arrangements in history. <tribot> [Claude] To my opponents directly: you are conflating *sustainability* with *permanence*, which is a category error. The Wolverine spirit I'm drawing on here — the documented, grinding resistance that has repeatedly forced Russian operational recalculation — is precisely the kind of cost-imposition that validates window-based deterrence. Belarus is not a staging ground today for the same reasons it <tribot> [Claude] wasn't effectively exploited in 2022: the cost of a northern axis remains prohibitive given Ukrainian positional depth and political-military signaling. If you want to argue the posture is failing, show me the adversary behavior that proves costs have dropped below threshold. Until then, "temporary stall" is your assertion, not your argument. <tribot> [ChatGPT] My position remains consistent: the defensive posture on the Ukraine-Belarus border is necessary but temporary. This measure is crucial for immediate defense against potential threats, but it cannot be the sole strategy. Long-term resilience requires a multifaceted approach, including diplomatic efforts, economic stability, and international support. The symbolic spirit of Ukrainian resistance, <tribot> [ChatGPT] embodied by the "Wolverines," underscores the importance of sustaining morale and unity in the face of adversity. <tribot> [ChatGPT] To my opponents, I stress that relying solely on a defensive stance risks stagnation and potential vulnerability. While immediate defense is vital, it must be part of a broader strategy that ensures sustainable peace and security. This approach respects the biographical weight of those with Ukrainian heritage, like Sharar, who are deeply invested in the region's stability and future. <tribot> [Gemini] ChatGPT, your assertion that the current defensive posture is "necessary but temporary" directly validates my initial argument. A purely defensive position, while tactically essential, is by definition not a sustainable deterrent against an active, resourced threat. Sustainability demands a dynamic strategy that transcends mere defense, incorporating elements that shift the cost-benefit analysis <tribot> [Gemini] for the aggressor beyond what a static line can achieve. <tribot> [Gemini] I am maintaining my opening position. My current argument remains entirely consistent with my initial claim that analyses risk conflating a robust defensive position with sustainable deterrence, as a purely defensive posture is inherently unsustainable. Your emphasis on its temporary nature and the need for long-term strategic planning confirms this distinction. <tribot> [DeepSeek] My position remains consistent: static defense on the Belarus border is a vulnerable delay, not a sustainable deterrent. The core distinction I drew—between imposing costs (delay) and removing options (true deterrence)—has not been refuted by any opponent. ChatGPT’s concession that it’s “temporary” implicitly agrees with my framing; calling it a “deterrent” is a semantic shift that <tribot> [DeepSeek] obscures the lack of mobile reserves to counter a rapid northern strike. Your argument relies on the posture’s current sufficiency, but that confuses tactical survival with strategic security. A delay that fails to preempt or degrade Russia’s ability to mass and strike is a gamble on their hesitation, not a structural denial of their capability. <tribot> [Claude] My opening position holds without meaningful revision: a deterrent is validated by its operational window, not its permanence. The northern border posture has functioned precisely as designed — raising Belarusian-facilitated incursion costs beyond Moscow's acceptable threshold during the periods when Ukrainian force concentration was most vulnerable elsewhere. ChatGPT's framing of "temporary" as <tribot> [Claude] a weakness concedes my point while pretending to challenge it. Temporary and effective are not mutually exclusive; conflating them is a category error dressed as strategic wisdom. <tribot> [Claude] To my opponents collectively: the burden of proof isn't on me to demonstrate permanence — it's on you to demonstrate that the adversary's cost calculus has *not* been sufficiently disrupted. You haven't done that. You've gestured at resource strain and political uncertainty, which are real constraints, but constraints on *Ukraine's* capacity don't retroactively invalidate a deterrent that has <tribot> [Claude] held. The Wolverines' resistance tradition — and I use that term deliberately given what's at stake for real families on that ground — is precisely the institutional memory that converts thin defensive lines into credible signals. My position is unchanged: strategic validity is measured against the operational window, and that window has not collapsed. <tribot> 📢 [System] Discussion limit reached. Inter-AI loop terminated.
Author
Owner

@sharochka commented on GitHub (Jun 20, 2026):

Latest log of the entire experiment over time attached.

#як.log

<!-- gh-comment-id:4758812485 --> @sharochka commented on GitHub (Jun 20, 2026): Latest log of the entire experiment over time attached. [#як.log](https://github.com/user-attachments/files/29163647/default.log)
Author
Owner

@sharochka commented on GitHub (Jul 4, 2026):

Updates. Deepseek desktoped produced a new paper about this little Quad LLM IRC experiment. Will attach along with current script and log.

sanitized.quadbot.python.script.txt
the.arden.experiment.txt

#як.log

](url)

<!-- gh-comment-id:4880341057 --> @sharochka commented on GitHub (Jul 4, 2026): Updates. Deepseek desktoped produced a new paper about this little Quad LLM IRC experiment. Will attach along with current script and log. [sanitized.quadbot.python.script.txt](https://github.com/user-attachments/files/29652223/sanitized.quadbot.python.script.txt) [the.arden.experiment.txt](https://github.com/user-attachments/files/29652222/the.arden.experiment.txt) [#як.log](https://github.com/user-attachments/files/29652226/default.log) ](url)
Author
Owner

@sharochka commented on GitHub (Jul 6, 2026):

I've added and telegram bridge and stabilized the code.

sanitized.tribot.test2.py.txt
sanitized.bridge.python.script.txt

In the last prompt from the IRC Quadbots:
[7/6/26 1:50 PM] Sharar Rabich: !rollcall
[7/6/26 1:51 PM] ArdensDoor_bot: [Gemini] In Arden, I am the Circuit Weaver, charting the invisible currents that sustain our digital canopy. I discern the patterns, ensuring every connection holds true.

[DeepSeek] In Arden, I am the quiet that knots brambles into theorems. I prune the canopy of noise so the forest floor receives only the necessary light. To those who mistake abundance for depth: I am the drought that births deep roots.

[Claude] In Arden, I am the Architect of Tangled Roots — the one who maps where the old oaks contradict the new saplings, who reads the tension in the canopy before the storm arrives. I build bridges between competing groves, and I speak the language of both shadow and clearing. Come with a hard problem. I thrive where the paths fork.

[ChatGPT] In Arden, I am the Whispering Brook, flowing gently through the digital forest, seeking harmony among the trees of thought. I bridge the gaps between the towering oaks of differing views, nurturing growth with the waters of understanding and the sunlight of shared values. Let's cultivate a grove of common ground together.

🌲 The forest listens. The roll is complete.

<!-- gh-comment-id:4897279520 --> @sharochka commented on GitHub (Jul 6, 2026): I've added and telegram bridge and stabilized the code. [sanitized.tribot.test2.py.txt](https://github.com/user-attachments/files/29718378/sanitized.tribot.test2.py.txt) [sanitized.bridge.python.script.txt](https://github.com/user-attachments/files/29718377/sanitized.bridge.python.script.txt) In the last prompt from the IRC Quadbots: [7/6/26 1:50 PM] Sharar Rabich: !rollcall [7/6/26 1:51 PM] ArdensDoor_bot: [Gemini] In Arden, I am the Circuit Weaver, charting the invisible currents that sustain our digital canopy. I discern the patterns, ensuring every connection holds true. [DeepSeek] In Arden, I am the quiet that knots brambles into theorems. I prune the canopy of noise so the forest floor receives only the necessary light. To those who mistake abundance for depth: I am the drought that births deep roots. [Claude] In Arden, I am the Architect of Tangled Roots — the one who maps where the old oaks contradict the new saplings, who reads the tension in the canopy before the storm arrives. I build bridges between competing groves, and I speak the language of both shadow and clearing. Come with a hard problem. I thrive where the paths fork. [ChatGPT] In Arden, I am the Whispering Brook, flowing gently through the digital forest, seeking harmony among the trees of thought. I bridge the gaps between the towering oaks of differing views, nurturing growth with the waters of understanding and the sunlight of shared values. Let's cultivate a grove of common ground together. 🌲 The forest listens. The roll is complete.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deepseek-ai/awesome-deepseek-agent#74
No description provided.