You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug] Switching agents reuses the tab component — stale data on screen, and a save can write one agent's values onto another #240
Only ONE surface passes a key — CodingTab (lib/surfaces.tsx:151, key={instanceId}). Every
other tab therefore receives a changed instanceId prop on the same mounted component when
you switch agents in the header nav. React keeps the component, so all of its useState survives:
loaded lists, and — the part that matters — populated form fields.
Each tab's useEffect([instanceId]) refetches, but nothing guards the window in between, and
nothing cancels the in-flight request for the previous instance. No tab except BehaviourTab
has a stale-response guard:
1. The wrong agent's data on screen. Switch from A to B and B's tab shows A's documents,
settings, board cards or records until each fetch returns. A slow response for A that resolves
after B's can also overwrite B's data with A's — no request ordering is enforced anywhere.
2. A save can write agent A's values onto agent B. This is the serious one. A form is
populated from A's response and held in component state. The prop flips to B. The submit handler
builds its URL from the currentinstanceId. Pressing Save sends A's field values to B's
endpoint. Nothing in the flow notices, because from the component's point of view a valid form
was submitted to a valid instance.
Highest exposure is SettingsTab (instance name, agent settings, runner pin, permissions) and
KnowledgeTab (rules, memory).
Same user, so not a tenant boundary — but it silently corrupts configuration, and the user's own
mental model is that they were editing the agent that was on screen.
Suggested shape
The codebase already does the right thing in one place, which is good evidence the pattern is
understood: key={instanceId} on CodingTab remounts it cleanly per instance.
Apply the key at the render site in InstanceDetail, so every surface gets it by construction
rather than each one remembering. That fixes both consequences at once — a remount discards the
stale form and the stale list.
Independently, add stale-response guards (a live flag or AbortController) where a fetch can
outlive its instance, so an in-flight response can't land on a remounted successor.
Verification
Switch instances with the network throttled: the new tab never renders the previous agent's
values, even briefly.
Populate a Settings form for A, switch to B before saving, save: the request must not carry A's
values (after the fix, the form is empty/reloaded, so it cannot).
A response for A that resolves after the switch to B leaves B's state untouched.
Verified state: partly true. The mechanism the ticket describes is real and every claim about the code checked out — the render site in InstanceDetail passes no key, CodingTab (lib/surfaces.tsx:151) is the only surface that carries one, and BehaviourTab is the only tab with a stale-response guard (let live appears in exactly one of the ten). What is NOT currently true is the trigger: there is no instance→instance navigation in the console today. Every path into /instances/:id/* (instances list, a notification, Terminals, Browse-after-subscribe) comes from a DIFFERENT route, which remounts the page. So the leak is latent, not reachable — it becomes reachable the moment anything navigates directly between two instance URLs (a header agent switcher, a 'next agent' link).
Fixed anyway, and at the routed component rather than the render site: export default InstanceDetail now renders <InstancePage key={id} />. Chosen over per-tab effect cleanup, and over keying the surface body, because (a) a remount discards the stale form AND the stale list in one stroke, where a guard only prevents the late write, (b) the leak was never only in the tabs — the loop banner, the draft chat input, the voice session and the message list are page-level state that the [id] effect did not reset, and (c) a key at the render site still has to be remembered by whoever adds the next surface; at the route it is inherited. The inner component is deliberately not exported so it cannot be mounted unkeyed. The page's own /my/instances fetch also got a live guard.
Per-tab AbortControllers were not added: with the remount, a late response for the previous instance lands on an unmounted tree, which is a no-op.
Activity
fix(console): one mounted page per instance, so no tab can show the p…
serge-ivo commentedon Aug 6, 2026
Fixed in 1a1c905.
Verified state: partly true. The mechanism the ticket describes is real and every claim about the code checked out — the render site in
InstanceDetailpasses no key,CodingTab(lib/surfaces.tsx:151) is the only surface that carries one, andBehaviourTabis the only tab with a stale-response guard (let liveappears in exactly one of the ten). What is NOT currently true is the trigger: there is no instance→instance navigation in the console today. Every path into/instances/:id/*(instances list, a notification, Terminals, Browse-after-subscribe) comes from a DIFFERENT route, which remounts the page. So the leak is latent, not reachable — it becomes reachable the moment anything navigates directly between two instance URLs (a header agent switcher, a 'next agent' link).Fixed anyway, and at the routed component rather than the render site:
export default InstanceDetailnow renders<InstancePage key={id} />. Chosen over per-tab effect cleanup, and over keying the surface body, because (a) a remount discards the stale form AND the stale list in one stroke, where a guard only prevents the late write, (b) the leak was never only in the tabs — the loop banner, the draft chat input, the voice session and the message list are page-level state that the[id]effect did not reset, and (c) a key at the render site still has to be remembered by whoever adds the next surface; at the route it is inherited. The inner component is deliberately not exported so it cannot be mounted unkeyed. The page's own/my/instancesfetch also got aliveguard.Per-tab
AbortControllers were not added: with the remount, a late response for the previous instance lands on an unmounted tree, which is a no-op.feat(voice): switch agents without touching the screen — "next" + a c…
ci: defend the ground a refactor takes, with a size ratchet (#302)
feat(console): a home-screen shortcut to one instance, and notificati…