What happens
InstanceDetail renders the active surface without a React key (pages/InstanceDetail.tsx:882):
const active = SURFACES.find((s) => s.id === tab);
const body = active.render({ instanceId: id, … });
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:
| Tab |
api calls |
stale guard |
| SettingsTab |
25 |
none |
| KnowledgeTab |
10 |
none |
| DataTab |
3 |
none |
| BoardTab / TmuxTab |
2 |
none |
| ActivityTab / IndexingTab / RepoTab / LoopRunsSection / TeamworkSection |
1 |
none |
| BehaviourTab |
1 |
live flag |
Two consequences
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 current instanceId. 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.
What happens
InstanceDetailrenders the active surface without a React key (pages/InstanceDetail.tsx:882):Only ONE surface passes a key —
CodingTab(lib/surfaces.tsx:151,key={instanceId}). Everyother tab therefore receives a changed
instanceIdprop on the same mounted component whenyou switch agents in the header nav. React keeps the component, so all of its
useStatesurvives:loaded lists, and — the part that matters — populated form fields.
Each tab's
useEffect([instanceId])refetches, but nothing guards the window in between, andnothing cancels the in-flight request for the previous instance. No tab except
BehaviourTabhas a stale-response guard:
liveflagTwo consequences
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 current
instanceId. Pressing Save sends A's field values to B'sendpoint. 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}onCodingTabremounts it cleanly per instance.InstanceDetail, so every surface gets it by constructionrather than each one remembering. That fixes both consequences at once — a remount discards the
stale form and the stale list.
liveflag orAbortController) where a fetch canoutlive its instance, so an in-flight response can't land on a remounted successor.
Verification
values, even briefly.
values (after the fix, the form is empty/reloaded, so it cannot).