fix: reject instance-served responses that outlive their instance
MCP status and skills were cleared on a switch but their in-flight requests were not, so a response for the previous instance could still write itself over the new one's — the same race the quota store already guards. Both now carry a generation. Usage also claimed an instance as loaded before it had answered, so a load that failed on a cold or briefly unreachable instance was never attempted again, and the previous instance's display mode and provider selection survived a switch — which decided what the new instance was even asked for.
This commit is contained in:
@@ -198,6 +198,10 @@ const SKILLS_LOAD_CACHE_TTL_MS = 5000;
|
||||
const DEFAULT_SKILLS_CACHE_KEY = '__default__';
|
||||
const skillsLastLoadedAt = new Map<string, number>();
|
||||
const skillsLoadInFlight = new Map<string, Promise<boolean>>();
|
||||
// Bumped on every runtime switch. Skills are discovered on the connected
|
||||
// instance and cached by directory, which two instances can share, so a load
|
||||
// already in flight for the previous instance must not write into the new one.
|
||||
let skillsGeneration = 0;
|
||||
|
||||
const getSkillsCacheKey = (directory: string | null): string => {
|
||||
return directory?.trim() || DEFAULT_SKILLS_CACHE_KEY;
|
||||
@@ -286,6 +290,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
skillDraft: null,
|
||||
|
||||
resetForRuntimeSwitch: () => {
|
||||
skillsGeneration += 1;
|
||||
skillsLastLoadedAt.clear();
|
||||
skillsLoadInFlight.clear();
|
||||
set({ skills: [], skillsByDirectory: {}, isLoading: false });
|
||||
@@ -316,6 +321,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const generation = skillsGeneration;
|
||||
const request = (async () => {
|
||||
set({ isLoading: true });
|
||||
// Failure must never look like an empty project. The mirror is the
|
||||
@@ -361,6 +367,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
data.externalSkills ?? null,
|
||||
);
|
||||
|
||||
if (generation !== skillsGeneration) return false;
|
||||
set((state) => {
|
||||
const next: Partial<SkillsStore> = {
|
||||
skillsByDirectory: { ...state.skillsByDirectory, [cacheKey]: visibleSkills },
|
||||
@@ -379,6 +386,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
}
|
||||
|
||||
console.error("Failed to load skills:", lastError);
|
||||
if (generation !== skillsGeneration) return false;
|
||||
set((state) => {
|
||||
const next: Partial<SkillsStore> = {
|
||||
skillsByDirectory: { ...state.skillsByDirectory, [cacheKey]: previousSkills },
|
||||
|
||||
Reference in New Issue
Block a user