Add ADR for live variant mode architecture

Comprehensive architecture decision record covering the live variant
mode: context, key decisions (source modification over DOM patching,
SSE over WebSocket, self-contained skill scripts, HTTP long-poll for
agent), full architecture diagram with message flows, variant wrapper
format, browser UI states, session persistence, security model,
server resilience, performance optimizations, test coverage, known
limitations, and future work.

Also picks up improvements from parallel thread: poll timeout bumped
to 10 min, SSE heartbeat every 30s, and other minor fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-13 14:16:07 -07:00
co-authored by Claude Opus 4.6
parent 59bb3d35a5
commit 9b573de1fb
37 changed files with 437 additions and 36 deletions
@@ -874,11 +874,15 @@
let evtSource = null;
let sseRetries = 0;
const SSE_MAX_RETRIES = 5;
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN);
evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect
};
evtSource.onmessage = (e) => {
sseRetries = 0; // reset on any successful message
let msg; try { msg = JSON.parse(e.data); } catch { return; }
@@ -93,7 +93,7 @@ Options:
// Poll mode: block until browser event
const timeoutArg = args.find(a => a.startsWith('--timeout='));
const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 120000;
const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000;
try {
const res = await fetch(`${base}/poll?token=${info.token}&timeout=${timeout}`);
@@ -24,7 +24,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
// PID file in the project root so both the server and agent can find it
// predictably (os.tmpdir() varies across platforms).
const LIVE_PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
const DEFAULT_POLL_TIMEOUT = 120_000;
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
// ---------------------------------------------------------------------------
// Port detection
@@ -206,7 +207,13 @@ function createRequestHandler({ detectScript, liveScriptWithToken }) {
state.sseClients.add(res);
clearTimeout(state.exitTimer);
// Keepalive: SSE comment every 30s prevents silent connection drops.
const heartbeat = setInterval(() => {
try { res.write(': keepalive\n\n'); } catch { clearInterval(heartbeat); }
}, SSE_HEARTBEAT_INTERVAL);
req.on('close', () => {
clearInterval(heartbeat);
state.sseClients.delete(res);
if (state.sseClients.size === 0) {
clearTimeout(state.exitTimer);