<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Richard</title>
    <description>The latest articles on DEV Community by Richard (@richard_0117).</description>
    <link>https://dev.to/richard_0117</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3911265%2F17299bc0-b565-4f84-ab17-d98b2cc440fc.png</url>
      <title>DEV Community: Richard</title>
      <link>https://dev.to/richard_0117</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richard_0117"/>
    <language>en</language>
    <item>
      <title>Building a 16-agent Socratic seminar in Tauri 2: bidding, paired observers, and a 0600 vault</title>
      <dc:creator>Richard</dc:creator>
      <pubDate>Mon, 04 May 2026 06:00:33 +0000</pubDate>
      <link>https://dev.to/richard_0117/building-a-16-agent-socratic-seminar-in-tauri-2-bidding-paired-observers-and-a-0600-vault-736</link>
      <guid>https://dev.to/richard_0117/building-a-16-agent-socratic-seminar-in-tauri-2-bidding-paired-observers-and-a-0600-vault-736</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/richer-richard/socratic-council" rel="noopener noreferrer"&gt;https://github.com/richer-richard/socratic-council&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Tauri 2 (Rust + React/TypeScript), pnpm monorepo, Apache-2.0&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Latest release:&lt;/strong&gt; v2.0.0&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;If you ask one frontier model a hard question, you get a confident answer. If you ask sixteen, you get an argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Socratic Council&lt;/strong&gt; is a desktop app that runs a structured seminar between sixteen LLM agents drawn from eight providers — OpenAI, Anthropic, Google, DeepSeek, Kimi, Qwen, MiniMax, and Z.AI. Eight named &lt;em&gt;debaters&lt;/em&gt; speak in public. Each is shadowed by a paired &lt;em&gt;advisor&lt;/em&gt; on the same provider that can pass private notes whenever they have something worth saying. As the debate runs, the app builds a live argument map (9 node kinds, 10 edge relations), flags fact-check candidates, tracks pairwise conflicts, and tallies per-message cost. Source on GitHub, Apache-2.0, source-only distribution.&lt;/p&gt;

&lt;p&gt;This is a "how I built it" post focused on four parts of the codebase that ended up more interesting than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;bidding protocol&lt;/strong&gt; — why every turn is scored, not round-robin.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;provider abstraction&lt;/strong&gt; — eight provider clients, one allowlisted egress.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;vault&lt;/strong&gt; — why it's a file with &lt;code&gt;0600&lt;/code&gt; perms, and not the macOS keychain.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;argument map&lt;/strong&gt; — a 9-kind / 10-relation schema with multi-source merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code excerpts below come straight from the repo at v2.0.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Bidding, not round-robin
&lt;/h2&gt;

&lt;p&gt;The default pattern when you glue agents together is round-robin: each agent takes a turn, in order, until you stop. It's simple. It's also why most multi-agent demos feel like reading a CS class group project — every voice gets equal floor time regardless of whether they have anything useful to say at the moment.&lt;/p&gt;

&lt;p&gt;Socratic Council picks the next speaker by &lt;strong&gt;relevance bidding&lt;/strong&gt;. Every turn, all eight council members are scored 0–100 against the topic and the recent transcript tail, and the highest score wins the turn. The scorer is a single fast call to a small model (Gemini 3.1 Flash by default; provider-injected, so it can be any of the eight):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/semanticBidding.ts (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are a neutral scheduler for a multi-agent debate.

Given a topic, a short tail of the recent conversation, and a list of debating agents (with their specialties), score each agent's relevance to speaking NEXT on a 0-100 scale.

Scoring rubric:
- 80-100: the agent's specialty is squarely at stake RIGHT NOW
- 50-79:  relevant but not the most pressing voice
- 20-49:  tangentially connected
- 0-19:   essentially unrelated to the current moment

Respond with exactly one JSON object on a single line, no prose, no code fences.
Shape: {"scores":{"agentId1":&amp;lt;int 0-100&amp;gt;, "agentId2":&amp;lt;int 0-100&amp;gt;, ...}}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version (&lt;code&gt;bidding.ts&lt;/code&gt;) used a hand-curated keyword list per agent and a substring match against the topic. That worked for prototype seminars about technology but missed everything when the topic was phrased without the keywords. The semantic-bidding pass replaces it with a single LLM judgment per turn — cheap (one Flash call) and dramatically more on-topic. The keyword score is kept as a fallback for transport failures.&lt;/p&gt;

&lt;p&gt;Crucially, this isn't the only protocol layer. While the chosen debater is generating in the public channel, &lt;strong&gt;all eight advisors evaluate the same transcript in parallel&lt;/strong&gt; and decide whether to slip a private note to their paired debater. Notes go into the partner's &lt;em&gt;next&lt;/em&gt; prompt under a section visible only to them — a literal index card in a fishbowl seminar.&lt;/p&gt;

&lt;p&gt;Two protocol details that took iteration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advisors share a provider with their debater.&lt;/strong&gt; George/Greta are both OpenAI. Cathy/Clara are both Anthropic. The pairing is on &lt;em&gt;role&lt;/em&gt;, not provider, so the asymmetry isn't "different model catches different errors" — it's "same model, different mandate." The advisor's job description is &lt;em&gt;read for what your partner is missing&lt;/em&gt;, and that prompt asymmetry produces useful notes even from the same weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisors can stay quiet.&lt;/strong&gt; The first version made every advisor emit something every turn, which produced a lot of "good point, partner" filler. Letting an advisor no-op when they have nothing real to add cleaned the side-channel up dramatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current full roster, from the README:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Debater&lt;/th&gt;
&lt;th&gt;Advisor&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Default model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;George&lt;/td&gt;
&lt;td&gt;Greta&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;GPT-5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cathy&lt;/td&gt;
&lt;td&gt;Clara&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Claude Opus 4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grace&lt;/td&gt;
&lt;td&gt;Gaia&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Douglas&lt;/td&gt;
&lt;td&gt;Dara&lt;/td&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kate&lt;/td&gt;
&lt;td&gt;Kira&lt;/td&gt;
&lt;td&gt;Kimi&lt;/td&gt;
&lt;td&gt;Kimi K2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quinn&lt;/td&gt;
&lt;td&gt;Quincy&lt;/td&gt;
&lt;td&gt;Qwen&lt;/td&gt;
&lt;td&gt;Qwen 3.6 Max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;Mila&lt;/td&gt;
&lt;td&gt;MiniMax&lt;/td&gt;
&lt;td&gt;MiniMax M2.7 Highspeed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zara&lt;/td&gt;
&lt;td&gt;Zoe&lt;/td&gt;
&lt;td&gt;Z.AI&lt;/td&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus a &lt;strong&gt;Moderator&lt;/strong&gt; — a system-role voice that opens the session, prompts the end-of-session ballot, and writes the synthesis. It defaults to Google but falls through the configured providers in order if Google isn't set up.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Eight provider clients, one allowlisted egress
&lt;/h2&gt;

&lt;p&gt;The tempting design for a multi-provider app is to write one client against an OpenAI-compatible interface and call it a day. I tried that. It collapses the moment you need any of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic's prompt-caching beta header&lt;/li&gt;
&lt;li&gt;OpenAI's Responses API (different shape from Chat Completions: &lt;code&gt;input&lt;/code&gt; not &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt; not system, &lt;code&gt;max_output_tokens&lt;/code&gt;, &lt;code&gt;reasoning.effort&lt;/code&gt; for o-series and GPT-5.x)&lt;/li&gt;
&lt;li&gt;Google's &lt;code&gt;x-goog-api-key&lt;/code&gt; header instead of &lt;code&gt;Authorization: Bearer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MiniMax's Anthropic-compatible endpoint&lt;/li&gt;
&lt;li&gt;Per-provider streaming envelope quirks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The codebase has a thin &lt;code&gt;BaseProvider&lt;/code&gt; interface and a per-provider client behind it. Headers are switched on provider type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/sdk/src/providers/base.ts (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;// Fix 4.5: include the prompt-caching beta header so `cacheControl: "ephemeral"`&lt;/span&gt;
  &lt;span class="c1"&gt;// on user messages actually engages the cache. Without this header&lt;/span&gt;
  &lt;span class="c1"&gt;// Anthropic ignores cache_control and bills every request as a full&lt;/span&gt;
  &lt;span class="c1"&gt;// re-prompt (5-10× cost overhead on long debates).&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;baseHeaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic-version&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2023-06-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic-beta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt-caching-2024-07-31&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing I am proud of here is that &lt;strong&gt;every provider client takes a custom &lt;code&gt;baseUrl&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/sdk/src/providers/openai.ts&lt;/span&gt;
&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Transport&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/responses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the credentials store models it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/shared/src/types/index.ts (excerpt)&lt;/span&gt;
&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nl"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nl"&gt;google&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// ... and so on for every provider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pairs with a deliberate exception in the Tauri-side network allowlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/desktop/src-tauri/src/allowlist.rs (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LOOPBACK_HOSTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"::1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"[::1]"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loopback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"http"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"https"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unsupported scheme '{}' for loopback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"https"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Scheme '{}' not allowed (https:// required)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_allowlisted_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Host '{}' is not on the IPC allowlist."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External hosts must be on a hardcoded provider allowlist &lt;em&gt;and&lt;/em&gt; must be &lt;code&gt;https://&lt;/code&gt;. &lt;strong&gt;Loopback is allowed &lt;code&gt;http://&lt;/code&gt;.&lt;/strong&gt; The unit tests spell out the intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;loopback_http_is_accepted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_outbound_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://127.0.0.1:11434/api/chat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_ok&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_outbound_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:11434/api/chat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_ok&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 11434 is Ollama's default. Pointing any provider seat at &lt;code&gt;http://localhost:11434/v1&lt;/code&gt; (or vLLM, or LM Studio, or &lt;code&gt;llama-server&lt;/code&gt; from llama.cpp) routes through the same Tauri command path the cloud calls use. There's also a 4 MB outbound body cap and a 600-request-per-60-seconds process-wide token bucket so a runaway loop can't melt anything.&lt;/p&gt;

&lt;p&gt;You can run a fully local seminar — every seat against a localhost endpoint — or mix local and frontier (DeepSeek-V3 on Ollama vs. Claude in the cloud) for an honest comparison. The seminar protocol surfaces where they actually disagree.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The vault: &lt;code&gt;0600&lt;/code&gt; file, not the keychain
&lt;/h2&gt;

&lt;p&gt;Every API key, every session transcript, every export goes through a 32-byte data-encryption key (DEK). The DEK lives in the platform's app-data directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS: &lt;code&gt;~/Library/Application Support/com.socratic-council.desktop/vault.key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Linux: &lt;code&gt;~/.local/share/.../vault.key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Windows: under &lt;code&gt;%APPDATA%&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;0600&lt;/code&gt; permissions on Unix; user-only ACL on Windows by default. The frontend reads the DEK once at boot via a Tauri command and uses it for the XChaCha20-Poly1305 envelope on every encrypted blob in localStorage.&lt;/p&gt;

&lt;p&gt;People who've shipped desktop apps will ask the obvious question: &lt;em&gt;why not the OS keychain?&lt;/em&gt; Especially on macOS, where &lt;code&gt;Security.framework&lt;/code&gt; would seem like the obvious move. The honest answer is in the file's own header comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/desktop/src-tauri/src/vault_file.rs (excerpt)&lt;/span&gt;
&lt;span class="cd"&gt;//! Rationale: macOS keychain access prompts the user for their login password&lt;/span&gt;
&lt;span class="cd"&gt;//! on every invocation when the binary has only an ad-hoc code signature,&lt;/span&gt;
&lt;span class="cd"&gt;//! because the keychain ACL system binds to a stable code signing identity&lt;/span&gt;
&lt;span class="cd"&gt;//! that ad-hoc simply doesn't provide. Result: ~15 password prompts per&lt;/span&gt;
&lt;span class="cd"&gt;//! launch as the frontend fetched one key per provider + the vault DEK.&lt;/span&gt;
&lt;span class="cd"&gt;//!&lt;/span&gt;
&lt;span class="cd"&gt;//! This module stores the 32-byte DEK in the platform's app-data directory&lt;/span&gt;
&lt;span class="cd"&gt;//! with user-only (0600 on unix) permissions.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A source-only OSS desktop app on macOS without a paid Apple Developer ID gets ad-hoc code-signed at build time. Ad-hoc has no stable identity, so every keychain &lt;code&gt;SecItemCopyMatching&lt;/code&gt; re-prompts. With one DEK fetch + one per-provider key fetch, that's ~15 password prompts per launch, every launch. No real user is going to put up with that. Filesystem perms on a per-user app-data file is the next stop down, and it's where we landed.&lt;/p&gt;

&lt;p&gt;The DEK lifecycle is small but careful. Three things in particular:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quarantine, not overwrite.&lt;/strong&gt; If the file exists but can't be read (corrupt, wrong size, FS error), it gets &lt;em&gt;moved aside&lt;/em&gt; to &lt;code&gt;vault.key.corrupt-&amp;lt;unix-ts&amp;gt;&lt;/code&gt; instead of silently overwritten. The user's previously-encrypted localStorage blobs are unrecoverable with the new DEK regardless, but at least a backup tool has a chance to repair the original later, and the frontend gets a &lt;code&gt;Quarantined&lt;/code&gt; status flag so it can warn the user explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Serialize)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"snake_case"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;VaultDekStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// Successfully read the existing DEK file.&lt;/span&gt;
    &lt;span class="n"&gt;Existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// No DEK file existed; a fresh one was created (typical first launch).&lt;/span&gt;
    &lt;span class="n"&gt;FreshlyCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// A DEK file existed but couldn't be read; quarantined and replaced.&lt;/span&gt;
    &lt;span class="cd"&gt;/// Encrypted blobs in localStorage from the prior DEK will fail to&lt;/span&gt;
    &lt;span class="cd"&gt;/// decrypt — surface a warning to the user.&lt;/span&gt;
    &lt;span class="n"&gt;Quarantined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Atomic create.&lt;/strong&gt; First-time creation uses &lt;code&gt;OpenOptions::create_new&lt;/code&gt; on a sibling tempfile, then &lt;code&gt;fs::rename&lt;/code&gt; to the real path. Two concurrent callers can't both write a different DEK in the same race. A power loss mid-write doesn't leave a half-written 16-byte file masquerading as a 32-byte DEK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reset requires a sentinel string.&lt;/strong&gt; The destructive command — &lt;code&gt;vault_reset&lt;/code&gt; — refuses to run unless the caller passes a literal confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;VAULT_RESET_CONFIRMATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DELETE-ALL-LOCAL-DATA"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;vault_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;confirmation&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;VAULT_RESET_CONFIRMATION&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"vault_reset requires confirmation parameter equal to '{}'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;VAULT_RESET_CONFIRMATION&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... delete vault.key&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defense-in-depth: a future UI button accidentally wired to &lt;code&gt;vault_reset&lt;/code&gt; without a typed user confirmation hits a 400-equivalent error rather than silently destroying everything. The frontend confirms with the user &lt;em&gt;and&lt;/em&gt; passes the sentinel; if either step is wrong the call refuses.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The argument map: 9 nodes, 10 relations, multi-source merge
&lt;/h2&gt;

&lt;p&gt;If you render a 16-agent debate as a chat log, it reads as noise within three turns. The argument map exists because the &lt;em&gt;artifact&lt;/em&gt; of a seminar should be the structure of the argument, not the transcript.&lt;/p&gt;

&lt;p&gt;The map is a directed graph maintained incrementally — after each council message, an extractor (Gemini 3.x by default) returns structured fragments, and a merger appends or merges them into the live graph. Schema v2 has nine node kinds and ten edge relations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/argmap.ts (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ArgNodeKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claim&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;premise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rebuttal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concession&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;question&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assumption&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;definition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proposal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ArgEdgeRelation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;supports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rebuts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concedes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refines&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agrees&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contradicts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;depends-on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;answers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;addresses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design choices worth flagging:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-source provenance.&lt;/strong&gt; When two different debaters assert effectively the same claim with different wording, the merger doesn't create two unrelated nodes — it merges them into one node carrying two &lt;code&gt;ArgNodeSource&lt;/code&gt; entries (with verbatim quotes and char offsets) and adds the new wording to the node's &lt;code&gt;aliases&lt;/code&gt;. The shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ArgNodeSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Char offsets into the source message content (optional). */&lt;/span&gt;
  &lt;span class="nl"&gt;span&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="cm"&gt;/** Verbatim quote from the source message (optional). */&lt;/span&gt;
  &lt;span class="nl"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same-claim merge is the bit that turns the graph from "transcript with extra steps" into a real consolidated artifact. The merging heuristic uses bag-of-words cosine — sufficient to catch "same claim, different wording" without pulling in an embedding model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stance polarity, status, verification.&lt;/strong&gt; Each node carries a &lt;code&gt;stance&lt;/code&gt; along a configured axis (e.g. &lt;code&gt;"central planning ↔ market"&lt;/code&gt;, polarity in &lt;code&gt;[-1, +1]&lt;/code&gt;), a &lt;code&gt;status&lt;/code&gt; (&lt;code&gt;active&lt;/code&gt; / &lt;code&gt;withdrawn&lt;/code&gt; / &lt;code&gt;superseded&lt;/code&gt;), and a &lt;code&gt;verification&lt;/code&gt; verdict from the fact-check pipeline. Edges carry a &lt;code&gt;confidence&lt;/code&gt; (0..1) and a one-line &lt;code&gt;rationale&lt;/code&gt;. None of this is cosmetic — the consolidation pass and the conflict graph both read these dimensions to decide what to highlight.&lt;/p&gt;

&lt;p&gt;The argument map exports as JSON, Mermaid, SVG, or PNG, independent of the full session export.&lt;/p&gt;




&lt;h2&gt;
  
  
  What didn't fit
&lt;/h2&gt;

&lt;p&gt;A few more bits worth pointing at if you fork the repo: the agent &lt;strong&gt;tool-calling DSL&lt;/strong&gt; — &lt;code&gt;@tool(oracle.search, {"query":"..."})&lt;/code&gt;, &lt;code&gt;@quote(george, "the exact line")&lt;/code&gt;, &lt;code&gt;@react(cathy, agree)&lt;/code&gt; — parsed inline and dispatched by the orchestrator; the &lt;strong&gt;&lt;code&gt;.scbundle&lt;/code&gt; export format&lt;/strong&gt;, a tarball of &lt;code&gt;session.json&lt;/code&gt;, &lt;code&gt;transcript.jsonl&lt;/code&gt;, &lt;code&gt;argmap.json&lt;/code&gt;, &lt;code&gt;synthesis.md&lt;/code&gt;, and &lt;code&gt;costs.csv&lt;/code&gt; that any other install can re-import without a cloud handoff; and the &lt;strong&gt;fairness module&lt;/strong&gt; that caps per-agent talk time so a single high-bidding seat can't monopolize. Read &lt;code&gt;packages/core/&lt;/code&gt; and &lt;code&gt;apps/desktop/src-tauri/src/&lt;/code&gt; for the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;A few honest notes for anyone building something in this neighborhood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The protocol matters more than the models.&lt;/strong&gt; The biggest quality jump in v2 wasn't swapping in more expensive providers — it was the relevance-bidding pass plus the paired-advisor side channel. Round-robin + symmetric agents was where the prototype was, and it was much worse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider-specific clients beat "OpenAI-compatible everything."&lt;/strong&gt; The compat shim looked attractive until I ran into Anthropic's prompt-caching, OpenAI's Responses API, and Google's auth. A thin per-provider client is more code but pays itself back in correctness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Tauri 2 over Electron is not even close in this category.&lt;/strong&gt; No bundled Chromium, native Rust crypto, smaller binaries, faster cold start. The webview-quirk surface is real but manageable for a desktop-only app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;0600&lt;/code&gt; is a fine answer to "where do I keep secrets."&lt;/strong&gt; I burned days trying to make the macOS keychain not prompt 15 times per launch on an ad-hoc-signed binary before accepting that filesystem perms + a quarantine recovery path was strictly better for this user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things I want to build next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A first-class &lt;strong&gt;"local mode" toggle&lt;/strong&gt; that wires every seat to a single local Ollama / vLLM endpoint with one click, instead of asking the user to set eight base URLs in Settings.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CRDT-backed argument map&lt;/strong&gt; so two people on the same LAN can co-watch a seminar with synced annotations.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;MCP server&lt;/strong&gt; that exposes a session as a tool for agents elsewhere — &lt;em&gt;"ask the seminar what it concluded about X."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Repo + license
&lt;/h2&gt;

&lt;p&gt;Source: &lt;strong&gt;&lt;a href="https://github.com/richer-richard/socratic-council" rel="noopener noreferrer"&gt;richer-richard/socratic-council&lt;/a&gt;&lt;/strong&gt; — Apache-2.0, v2.0.0 just shipped. macOS quick install via &lt;code&gt;install.sh&lt;/code&gt;; Windows/Linux via &lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm tauri:build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have feedback on the bidding protocol, the provider abstraction, or the vault design — I'd much rather hear it now, while v2 is fresh, than after I've cemented the wrong decisions in v3. Issues open, PRs welcome.&lt;/p&gt;

&lt;p&gt;— Richard&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tauri</category>
      <category>showdev</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
