Skip to content

[bug] Switching agents reuses the tab component — stale data on screen, and a save can write one agent's values onto another #240

Description

@serge-ivo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions