June 30, 2026

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 2addMessage 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 3subscribeSession 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 driftshellSessionId 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 scopecallToolGated 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