Create docs/mockups/01-option-2-piyush-style.html
5bf232170827 jacobcole 2026-04-23 1 file
new file mode 100644
index 0000000..8fe5130
@@ -0,0 +1,348 @@
+---
+visibility: public
+---
+
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>picortex — Option 2 · Piyush-style</title>
+<style>
+ :root{
+ --bg:#0a0a0a; --surface:#161616; --elev:#1e1e1e; --border:#2a2a2a;
+ --text:#e8e8e8; --muted:#8a8a8a; --mutedless:#bdbdbd;
+ --accent:#8b6dff; --accent-soft:#3a2f66;
+ --ok:#30d158; --warn:#ffd60a; --bad:#ff453a;
+ --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
+ --sans:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro","Inter","Helvetica Neue",Arial,sans-serif;
+ --msg-me:#0a84ff; --msg-them:#2c2c2e; --msg-bot:#1b3a35; --msg-bot-bd:#215c52;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased;line-height:1.5}
+ a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline}
+ code{font-family:var(--mono);background:var(--elev);padding:1px 6px;border-radius:4px;font-size:.9em}
+ pre{font-family:var(--mono);background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px 16px;overflow-x:auto;font-size:.84rem;color:var(--mutedless);margin:12px 0}
+ nav.top{position:sticky;top:0;z-index:10;background:rgba(10,10,10,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--border)}
+ nav.top .inner{max-width:1100px;margin:0 auto;padding:14px 22px;display:flex;gap:18px;align-items:center;flex-wrap:wrap}
+ nav.top .brand{font-weight:700;letter-spacing:-.01em} nav.top .brand .dot{color:var(--accent)}
+ nav.top .links{display:flex;gap:14px;flex-wrap:wrap}
+ nav.top .links a{color:var(--mutedless);font-size:.92rem} nav.top .links a.current{color:var(--text)}
+ nav.top .badge{margin-left:auto;font-size:.78rem;color:var(--muted);border:1px solid var(--border);padding:3px 8px;border-radius:999px}
+ main{max-width:1100px;margin:0 auto;padding:44px 22px 100px}
+ h1{font-size:2.3rem;margin:0 0 12px;letter-spacing:-.02em;line-height:1.15}
+ h1 .tag{color:var(--accent);font-weight:500;font-size:.9rem;letter-spacing:.08em;text-transform:uppercase;display:block;margin-bottom:8px}
+ h2{font-size:1.45rem;margin:50px 0 14px;letter-spacing:-.015em}
+ h2 .num{display:inline-block;width:28px;height:28px;line-height:28px;text-align:center;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:.85rem;margin-right:10px;vertical-align:middle}
+ h3{font-size:1.02rem;margin:22px 0 10px;color:var(--mutedless);font-weight:600}
+ p.lead{font-size:1.08rem;color:var(--mutedless);max-width:68ch;margin:0 0 18px}
+ p{max-width:72ch;color:var(--mutedless)}
+ .hr{height:1px;background:var(--border);margin:48px 0}
+
+ .quote-box{border-left:3px solid var(--accent);background:linear-gradient(180deg,rgba(139,109,255,.05),rgba(139,109,255,0));padding:16px 22px;border-radius:0 8px 8px 0;margin:14px 0}
+ .quote-box .label{font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-bottom:4px}
+ .quote-box p{color:var(--text);margin:4px 0}
+
+ .arch-wrap{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:22px;overflow-x:auto}
+ .arch-wrap svg{display:block;margin:0 auto;max-width:100%;height:auto}
+ .legend{display:flex;flex-wrap:wrap;gap:14px;margin:18px 0 4px;font-size:.85rem;color:var(--mutedless)}
+ .legend .dot{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:6px;vertical-align:middle}
+
+ .cols2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
+ @media (max-width:760px){.cols2{grid-template-columns:1fr}}
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px}
+ .card.ok{border-left:3px solid var(--ok)}
+ .card.warn{border-left:3px solid var(--warn)}
+ .card.bad{border-left:3px solid var(--bad)}
+ .card h3{margin-top:0}
+ .card ul{margin:6px 0;padding-left:22px} .card li{color:var(--mutedless);margin:3px 0}
+
+ .flow{counter-reset:step;padding:0;margin:18px 0;list-style:none;display:grid;grid-template-columns:1fr;gap:8px}
+ .flow li{counter-increment:step;display:flex;gap:14px;align-items:flex-start;padding:12px 14px;background:var(--surface);border:1px solid var(--border);border-radius:10px}
+ .flow li::before{content:counter(step);flex-shrink:0;width:26px;height:26px;line-height:26px;text-align:center;border-radius:50%;background:var(--accent);color:#fff;font-size:.82rem;font-weight:600}
+ .flow li .text{flex:1;font-size:.93rem;color:var(--mutedless)}
+ .flow li .text b{color:var(--text)}
+ .flow li code{font-size:.82rem}
+
+ table{width:100%;border-collapse:collapse;margin:14px 0;font-size:.92rem}
+ th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);color:var(--mutedless)}
+ th{color:var(--text);font-weight:600;background:var(--surface)}
+ tr:last-child td{border-bottom:0}
+
+ footer{max-width:1100px;margin:0 auto;padding:36px 22px 80px;color:var(--muted);font-size:.85rem;border-top:1px solid var(--border)}
+ footer .row{display:flex;gap:22px;flex-wrap:wrap}
+</style>
+</head>
+<body>
+
+<nav class="top"><div class="inner">
+ <span class="brand">picortex<span class="dot">.</span></span>
+ <div class="links">
+ <a href="00-platonic-ideal.html">Platonic ideal</a>
+ <a class="current" href="01-option-2-piyush-style.html">Option 2 · Piyush-style</a>
+ <a href="02-option-4-noos-style.html">Option 4 · Noos-style</a>
+ <a href="https://wikihub.globalbr.ai/@jacobcole/picortex/docs/prd/002-texting-experience">PRD 002 ↗</a>
+ </div>
+ <span class="badge">concrete · 3 machines</span>
+</div></nav>
+
+<main>
+
+<h1><span class="tag">Mockup 01 · Option 2</span>Piyush-style (two-box, <code>claude -c -p</code> per turn)</h1>
+<p class="lead">The <a href="00-platonic-ideal.html">platonic ideal</a> realized as three machines talking HTTPS + SSH. Derived from the pre-container Cortex design Piyush Jha shipped in Jan 2026, adapted for per-chat isolation and the P4 consent-loop. No tmux; no long-lived REPLs; no shared-host bot-workspace coupling.</p>
+
+<div class="quote-box">
+ <div class="label">TL;DR architectural difference</div>
+ <p>Bot lives on machine A. Per-chat workspaces live on machine B (separate VPS, Linux users, <code>0700</code> home dirs). Knowledge graph lives on machine C. One <code>ssh&nbsp;→&nbsp;claude -c -p</code> per turn. Bot Gateway's crash has zero effect on in-flight Claude sessions.</p>
+</div>
+
+<p><b>UX is the same as the platonic ideal.</b> This page focuses on the mechanical architecture that delivers it. For the user-facing scenes (attention, consent loop, manifest dialog, reactions), see <a href="00-platonic-ideal.html">Mockup 00</a>.</p>
+
+<h2><span class="num">1</span>The three machines</h2>
+
+<div class="arch-wrap">
+<svg viewBox="0 0 1000 620" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Option 2 architecture">
+ <defs>
+ <marker id="arrA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#8b6dff"/>
+ </marker>
+ <marker id="arrM" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#6a6a6a"/>
+ </marker>
+ <marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#6fd07a"/>
+ </marker>
+ </defs>
+ <style>
+ .machine{fill:#12141a;stroke:#30385a;stroke-width:1.5;rx:14;ry:14}
+ .svc{fill:#1e1e1e;stroke:#2a2a2a;stroke-width:1;rx:8;ry:8}
+ .svc.hot{stroke:#8b6dff}
+ .svc.chat{fill:#171a2e;stroke:#324a6a}
+ .svc.dm{fill:#12201c;stroke:#215c52}
+ .title{fill:#e8e8e8;font:600 13px -apple-system,Inter,sans-serif}
+ .mtitle{fill:#bdbdbd;font:500 11px -apple-system,Inter,sans-serif;letter-spacing:.04em;text-transform:uppercase}
+ .sub{fill:#8a8a8a;font:500 10.5px -apple-system,Inter,sans-serif}
+ .code{fill:#b3a6ff;font:500 10.5px ui-monospace,monospace}
+ .edge{stroke:#8b6dff;stroke-width:1.6;fill:none}
+ .edge.ssh{stroke:#6fd07a}
+ .edge.muted{stroke:#6a6a6a;stroke-dasharray:4 4}
+ .elabel{fill:#bdbdbd;font:500 10.5px ui-monospace,monospace}
+ </style>
+
+ <!-- user endpoints -->
+ <rect class="svc" x="10" y="16" width="170" height="48" rx="10"/>
+ <text class="title" x="95" y="38" text-anchor="middle">Group chat</text>
+ <text class="sub" x="95" y="54" text-anchor="middle">iMessage / OpenChat</text>
+
+ <rect class="svc dm" x="815" y="16" width="175" height="48" rx="10"/>
+ <text class="title" x="902" y="38" text-anchor="middle">Jacob's DM</text>
+ <text class="sub" x="902" y="54" text-anchor="middle">out-of-band approval</text>
+
+ <!-- Machine A -->
+ <rect class="machine" x="30" y="100" width="420" height="370"/>
+ <text class="mtitle" x="240" y="124" text-anchor="middle">MACHINE A · Bot Gateway</text>
+ <text class="sub" x="240" y="139" text-anchor="middle">Hetzner VPS · 2 vCPU · 4 GB RAM · Ubuntu 22.04</text>
+
+ <rect class="svc hot" x="55" y="160" width="370" height="56"/>
+ <text class="title" x="240" y="183" text-anchor="middle">Linq / OpenChat channel adapter</text>
+ <text class="sub" x="240" y="200" text-anchor="middle">HMAC verify · dedup · normalize event vocabulary</text>
+
+ <rect class="svc" x="55" y="232" width="175" height="56"/>
+ <text class="title" x="143" y="255" text-anchor="middle">Attention gate</text>
+ <text class="sub" x="143" y="272" text-anchor="middle">mentions / discriminator</text>
+
+ <rect class="svc" x="250" y="232" width="175" height="56"/>
+ <text class="title" x="337" y="255" text-anchor="middle">Turn dispatcher</text>
+ <text class="sub" x="337" y="272" text-anchor="middle">queue · concurrency · retry</text>
+
+ <rect class="svc dm" x="55" y="304" width="370" height="56"/>
+ <text class="title" x="240" y="327" text-anchor="middle">Consent broker</text>
+ <text class="sub" x="240" y="344" text-anchor="middle">in-memory pause · DM Jacob · timer · Disclosure audit</text>
+
+ <rect class="svc" x="55" y="376" width="370" height="56"/>
+ <text class="title" x="240" y="399" text-anchor="middle">SQLite canonical log</text>
+ <text class="sub" x="240" y="416" text-anchor="middle">chats · messages · manifests · DisclosureEvent · ManifestEvent</text>
+
+ <!-- Machine B -->
+ <rect class="machine" x="550" y="100" width="430" height="310"/>
+ <text class="mtitle" x="765" y="124" text-anchor="middle">MACHINE B · Workspace host</text>
+ <text class="sub" x="765" y="139" text-anchor="middle">Hetzner VPS · 4 vCPU · 8 GB RAM · Linux users per chat</text>
+
+ <rect class="svc chat" x="575" y="160" width="380" height="52"/>
+ <text class="title" x="765" y="182" text-anchor="middle">picortex service user (admin)</text>
+ <text class="sub" x="765" y="199" text-anchor="middle">sudoers: useradd, userdel, chown, chmod, runuser · NO bash</text>
+
+ <rect class="svc chat" x="575" y="224" width="182" height="52"/>
+ <text class="title" x="666" y="246" text-anchor="middle">chat-a1b2 (home 0700)</text>
+ <text class="sub" x="666" y="263" text-anchor="middle">.claude/ · workspace/ · .picortex/</text>
+
+ <rect class="svc chat" x="773" y="224" width="182" height="52"/>
+ <text class="title" x="864" y="246" text-anchor="middle">chat-c3d4 (home 0700)</text>
+ <text class="sub" x="864" y="263" text-anchor="middle">.claude/ · workspace/ · .picortex/</text>
+
+ <rect class="svc" x="575" y="288" width="380" height="56"/>
+ <text class="title" x="765" y="308" text-anchor="middle">On turn: <tspan class="code">runuser -u chat-X -- claude --session-id X -p "&lt;text&gt;"</tspan></text>
+ <text class="sub" x="765" y="326" text-anchor="middle">stdout → Bot Gateway · session continuity in ~/.claude/ on this host</text>
+
+ <!-- Machine C -->
+ <rect class="machine" x="160" y="500" width="680" height="94"/>
+ <text class="mtitle" x="500" y="524" text-anchor="middle">MACHINE C · noos graph</text>
+ <text class="sub" x="500" y="539" text-anchor="middle">already deployed on Lightsail · HTTPS · x-api-key · read-only for picortex</text>
+
+ <rect class="svc" x="190" y="546" width="620" height="36"/>
+ <text class="title" x="500" y="569" text-anchor="middle">GET /api/nodes?tags=...&amp;limit=... · POST /api/nodes (writes later, with approval)</text>
+
+ <!-- edges -->
+ <!-- group chat ↔ channel adapter -->
+ <path class="edge" marker-end="url(#arrA)" marker-start="url(#arrA)" d="M 95,64 L 95,160"/>
+ <text class="elabel" x="105" y="100">HMAC webhook / sendMessage</text>
+
+ <!-- Jacob DM ↔ channel adapter (via consent) -->
+ <path class="edge muted" marker-end="url(#arrM)" marker-start="url(#arrM)" d="M 902,64 C 902,250 700,320 425,332"/>
+ <text class="elabel" x="600" y="160">out-of-band DM (same channel)</text>
+
+ <!-- A → B (ssh) -->
+ <path class="edge ssh" marker-end="url(#arrG)" d="M 425,260 L 575,260"/>
+ <text class="elabel" x="500" y="250" text-anchor="middle">ssh -i key chat-X@B</text>
+
+ <!-- A → C (HTTPS) -->
+ <path class="edge" marker-end="url(#arrA)" d="M 240,432 L 240,500"/>
+ <text class="elabel" x="255" y="470">HTTPS · noos API</text>
+
+ <!-- B → C (optional: from inside Claude tool-use) -->
+ <path class="edge muted" marker-end="url(#arrM)" d="M 765,344 L 765,500"/>
+ <text class="elabel" x="780" y="420">optional MCP · noos</text>
+</svg>
+<div class="legend">
+ <span><span class="dot" style="background:#12141a;border:1px solid #30385a"></span>machine</span>
+ <span><span class="dot" style="background:#1e1e1e;border:1px solid #2a2a2a"></span>service</span>
+ <span><span class="dot" style="background:#171a2e;border:1px solid #324a6a"></span>per-chat user</span>
+ <span><span class="dot" style="background:#12201c;border:1px solid #215c52"></span>out-of-band path</span>
+ <span><span class="dot" style="background:#8b6dff"></span>HTTPS / in-channel</span>
+ <span><span class="dot" style="background:#6fd07a"></span>SSH</span>
+</div>
+</div>
+
+<h2><span class="num">2</span>What lives where</h2>
+
+<table>
+ <thead><tr><th>Component</th><th>Machine</th><th>Process / store</th><th>Cost of failure</th></tr></thead>
+ <tbody>
+ <tr><td>Channel adapter + Bot Gateway</td><td>A</td><td>Fastify service, systemd</td><td>Inbound queue stalls; no data loss (Linq retries).</td></tr>
+ <tr><td>SQLite canonical log</td><td>A (local disk)</td><td><code>/var/lib/picortex/picortex.sqlite</code></td><td>Fatal. Daily <code>litestream</code> → S3.</td></tr>
+ <tr><td>Consent broker (pause state)</td><td>A</td><td>In-memory + SQLite rehydration row</td><td>Recovered from SQLite on restart; pending group waits survive.</td></tr>
+ <tr><td>Per-chat Linux user + home dir</td><td>B</td><td><code>/srv/picortex/chats/&lt;chat&gt;</code></td><td>Only that chat's context/memory lost if B is wiped.</td></tr>
+ <tr><td>Claude session memory</td><td>B</td><td><code>~chat-X/.claude/</code></td><td>Per-chat. Each chat resurrects with fresh memory; transcript rebuild possible from A's log.</td></tr>
+ <tr><td>noos graph</td><td>C</td><td>Neo4j (existing Lightsail deploy)</td><td>Bot degrades gracefully: "I can't reach my knowledge right now."</td></tr>
+ </tbody>
+</table>
+
+<h2><span class="num">3</span>Concrete consent-loop turn (wire view)</h2>
+
+<ol class="flow">
+ <li><div class="text"><b>A · inbound webhook.</b> Linq POSTs <code>message.received</code> for "is jacob free tuesday?" to <code>https://picortex.globalbr.ai/api/linq/inbound</code>. HMAC verified. Row inserted in <code>messages</code> on A's SQLite.</div></li>
+ <li><div class="text"><b>A · attention gate passes</b> (@mention detected). A · Turn dispatcher checks the chat's manifest row: <code>calendar</code> not in scope.</div></li>
+ <li><div class="text"><b>A · consent broker activates.</b> Inserts <code>DisclosureEvent (approval_mode: pending, ttl_expires_at: +10min)</code>. Sends group ack via Linq sendMessage: "Let me check with him — one sec." Typing indicator on.</div></li>
+ <li><div class="text"><b>A · out-of-band DM</b> to Jacob via Linq sendMessage with full context + proposed response + approve/deny keys.</div></li>
+ <li><div class="text"><b>A · Jacob replies <code>y</code>.</b> Broker matches to the pending DisclosureEvent, marks <code>approval_mode: approve-exact</code>, expands turn scope to include <code>calendar:2026-04-28T19:00/21:00</code>.</div></li>
+ <li><div class="text"><b>A → B · SSH.</b> Turn dispatcher opens SSH to machine B:<br><code>ssh -i /etc/picortex/keys/b_admin.pem picortex@B<br>sudo -u chat-a1b2 -H claude --session-id chat-a1b2 -p "&lt;system+context+prompt&gt;"</code></div></li>
+ <li><div class="text"><b>B · Claude runs.</b> Reads <code>~/.claude/sessions/chat-a1b2/</code> for prior turns on this chat. Calls optional noos MCP tool to check calendar. stdout = proposed reply.</div></li>
+ <li><div class="text"><b>A · outbound.</b> Bot Gateway receives stdout over SSH, writes to SQLite, sends final reply to group via Linq sendMessage. Closes DisclosureEvent with <code>final_reply</code> + <code>grant_ttl: single-use</code>.</div></li>
+</ol>
+
+<h2><span class="num">4</span>Code sketch (the Bot Gateway's executor)</h2>
+
+<pre><code>// MachineA/src/executor.ts — per-turn dispatcher (Option 2)
+import { SSHClient } from './ssh.js'
+import { scopedCtx } from './manifest.js'
+
+export async function executeTurn(chat: Chat, message: Message, scope: Scope) {
+ const turnId = ulid()
+ const ctxSystem = buildSystemPrompt(chat, scope) // manifest-filtered
+ const prompt = formatTurnInput(message)
+
+ // One SSH exec per turn. No tmux, no sentinels.
+ const { stdout, code } = await ssh.execAsUser(
+ chat.unix_user,
+ ['claude', '--session-id', chat.id, '-p', '--dangerously-skip-permissions', '--system', ctxSystem, prompt],
+ { env: { PICORTEX_TURN_ID: turnId }, timeout_ms: 120_000 },
+ )
+
+ if (code !== 0) return replyFailure(stdout)
+ return recordAndSend(chat, stdout, { turnId })
+}
+</code></pre>
+
+<h3>Consent broker — pause/resume</h3>
+<pre><code>// On pause: write DisclosureEvent row, send DM, return to event loop.
+// On Jacob's reply: match by (jacob_dm_chat_id, in-flight-row), re-invoke executeTurn with expanded scope.
+// On timeout: clear row, send group "I need to check on that offline", log approval_mode="timeout".
+</code></pre>
+
+<h2><span class="num">5</span>Strengths &amp; costs, head-to-head</h2>
+
+<div class="cols2">
+ <div class="card ok">
+ <h3>Strengths</h3>
+ <ul>
+ <li><b>Privacy split matches the threat model.</b> A Claude turn that gets prompt-injected into "cat ~/.ssh/id_rsa" runs on B; the DM-approval state and Linq webhook secret live on A. Two independent blast radii.</li>
+ <li><b>Crash safety.</b> A can restart mid-turn without losing Claude's memory (it's on B). B can be rebooted per-chat-user without affecting the bot.</li>
+ <li><b>No tmux fragility.</b> No sentinel protocol. <code>-p</code> + stdout is the entire reply-capture contract.</li>
+ <li><b>Linear cost.</b> ~$10/mo machine A + ~$15/mo machine B + free machine C (shared with noos).</li>
+ <li><b>Matches existing deploy patterns.</b> Machine B = a stripped-down jcortex clone. Machine A = same systemd+Caddy pattern as voice-assistant.</li>
+ </ul>
+ </div>
+ <div class="card warn">
+ <h3>Costs / risks</h3>
+ <ul>
+ <li><b>Two machines to maintain.</b> Provisioning, keys, firewall rules, upgrades — doubled.</li>
+ <li><b>SSH setup burden.</b> ed25519 key distribution, <code>authorized_keys</code> hygiene, monitored sudoers drop-in. Manageable but not zero.</li>
+ <li><b>Latency floor bumped by SSH.</b> ~80-200 ms round-trip overhead per turn vs a single-box architecture. Warm reply still within budget (&lt;15 s).</li>
+ <li><b>B must stay online</b> for turns to complete. If B is down, turns queue on A; Linq retry saves us.</li>
+ <li><b>Claude CLI behavior dependency.</b> <code>--session-id</code> contract + <code>~/.claude/</code> layout are upstream-owned. Drift = work.</li>
+ </ul>
+ </div>
+</div>
+
+<h2><span class="num">6</span>Five failure modes &amp; what happens</h2>
+
+<table>
+ <thead><tr><th>Failure</th><th>User-visible</th><th>Recovery</th></tr></thead>
+ <tbody>
+ <tr><td>A restart mid-turn</td><td>Brief typing indicator gap; bot posts "back online, here's that answer" if turn was in consent-loop pause</td><td>Rehydrate paused DisclosureEvents from SQLite on boot</td></tr>
+ <tr><td>B down</td><td>Bot: "I'm briefly unable to think — one moment" in DM; groups get nothing until B returns</td><td>Retry turn once B is up; Linq retries inbound for up to 24 h</td></tr>
+ <tr><td>C (noos) down</td><td>Bot: "I can't reach my knowledge graph right now — try again?"</td><td>Attention gate still works; non-graph questions still answered</td></tr>
+ <tr><td>SSH key rotation mid-flight</td><td>Single turn fails loudly; reply is "I had a glitch, try again"</td><td>Key reloaded from Vault/env on next turn</td></tr>
+ <tr><td>Claude CLI OOMs on B</td><td>Turn errors out; reply "Jacob's bot had a glitch"</td><td>cgroup memory limit per chat user prevents cross-chat impact</td></tr>
+ </tbody>
+</table>
+
+<h2><span class="num">7</span>Why this might be the answer</h2>
+<div class="quote-box">
+ <p>Codex's session review flagged S3 (single shared tmux) and the tmux sentinel protocol as the weakest parts of the original plan. Piyush already ran the spike we were being asked to run — <code>claude -c -p</code> per turn, via SSH, with session memory in the workspace's own <code>~/.claude/</code>. It worked. The machines are cheap, the isolation story matches the threat model, and the consent-loop broker pattern composes naturally on top.</p>
+</div>
+
+<h2><span class="num">8</span>What this doesn't solve</h2>
+<ul>
+ <li>Does not protect against a compromised <em>noos</em> (machine C) leaking data. That's a separate trust boundary.</li>
+ <li>Does not eliminate shared-kernel risk on B. If the D2 isolation report finds Linux-user perms insufficient for the real threat model, B's per-chat users get wrapped in <code>bubblewrap</code> or Landlock — composes naturally, no re-architecture.</li>
+ <li>Does not give you an ephemeral per-chat machine (Option 3). If that becomes the real privacy requirement, B's per-chat users are replaced with Fly Machines per chat.</li>
+</ul>
+
+<div class="hr"></div>
+
+<p style="text-align:center;color:var(--muted);font-size:.92rem">Compare with the platonic ideal (<a href="00-platonic-ideal.html">Mockup 00</a>) or the alternative Option 4 stateless design (<a href="02-option-4-noos-style.html">Mockup 02</a>).</p>
+
+</main>
+
+<footer>
+ <div class="row">
+ <span>picortex · mockup 01 · Option 2 · 2026-04-23</span>
+ <a href="https://wikihub.globalbr.ai/@jacobcole/picortex/docs/wiki/piyush-era-design">Piyush-era study ↗</a>
+ <a href="https://wikihub.globalbr.ai/@jacobcole/picortex/docs/plans/2026-04-23-prototype-options">Prototype options ↗</a>
+ <a href="https://wikihub.globalbr.ai/@jacobcole/picortex/docs/prd/002-texting-experience">PRD 002 ↗</a>
+ </div>
+</footer>
+
+</body>
+</html>
\ No newline at end of file