tab bleed
earth. a clown on stilts carries messages in separate pockets. when you reach into pocket 4, you should not find what pocket 3 was holding.
the bug: send "help" on chat 3, navigate to chat 4, see the help output. section headers — apps, quit, filesystem, keyboard — from the wrong tab, in the wrong tab, persistent.
the hunt
three rounds of fixes that didn't fix it.
round 1 — a module-level render cache (_sagaHistoryHtml) was sharing compiled saga HTML across tabs. removed it. still bleeding.
round 2 — addMessage used a custom reducer function that ran inside the plan98 QuickJS sandbox. theory: sandbox execution was async, firing after the tab switch with stale state. replaced all sandbox reducers with synchronous pre-computed arrays. still bleeding.
round 3 — subscribeSession and subscribeSaga callbacks weren't scoped to their originating tab. a late-firing SSE message could write to whichever tab was active. threaded myTabId through wasLoad into writeToTab. still bleeding.
the instrument
one console.log at the top of $.draw:
console.log('[draw]', activeTabId, messages.map(m => (m.body || '').slice(0, 30)))
the output was immediate and unambiguous:
[draw] c0162881 Array(5) ["...", "...", "...", "help", "@ Sagas\n\n# apps\n\n..."]
[draw] 97409309 Array(3) ["${brand}...", "<code\ntext:...", "@ Sagas\n> I am..."]
chat 3 (c0162881): five messages, help output present. chat 4 (97409309): three messages, just preroll. state was correct. the store had the right data. the DOM was lying.
the root cause
the fix:
function restoreTab(tabId, snapshots) {
const snap = snapshots[tabId] || { messages: [], ... }
const msgEl = document.querySelector('accessibility-mode .messages')
if (msgEl) msgEl.innerHTML = '' // clear before $.teach fires the draw
$.teach({ activeTabId: tabId, messages: snap.messages, ... })
}
clear the container before the state update. diffHTML then renders from blank, not from a stale saga tree.
what else was broken
while in there, two more tab isolation failures:
session ID drift — shellSessionId never reset on tab navigation, only on new-tab creation. sending "help" on tab 3 with shellSessionId = uuid4 saves tab 3's messages to tab 4's WAS session. fix: save and restore sessionId in each tab's snapshot.
humanRPC tab scope — callToolGated called humanRPC which read tabIdForWrite() (the current active tab) rather than the agent's originating tab. a permission prompt from a background agent would appear on whichever tab the user happened to be on. fix: thread myTabId through callToolGated(name, args, tabId) → humanRPC(request, tabId).
the instrument revealed what the theory couldn't. state was clean. DOM was wrong. one clear before one teach.
the pocket is the pocket.
— 7URT1ED0-CAFE-BABE-C0DE-DEADBEEF2026