When async flows drive UI state (events, config reads, resume/load), never assume timing/order will be favorable. Make initialization idempotent and conditionally applied so missed/early events or late async completions can’t leave the UI stuck or clobber user intent.

Apply these rules: 1) Provide a “catch-up” path for state that may already be loaded.

2) Guard against out-of-order async updates.

Example pattern (session event + immediate catch-up):

const loadForSession = (id: string) => {
  // load extensions for id (idempotent)
};

const onLoaded = (event: Event) => {
  const targetSessionId = (event as CustomEvent<{ sessionId?: string }>).detail?.sessionId;
  if (targetSessionId !== sessionId) return;
  loadForSession(targetSessionId!);
};

window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, onLoaded);
// catch-up: don’t rely solely on whether the event already fired
loadForSession(sessionId);

Example pattern (async config read without clobbering user choice):

let userTouched = false;

// set userTouched = true in your dropdown onChange

const effortFromConfig = await readConfig('SOME_EFFORT_KEY');
if (effortFromConfig && !userTouched) {
  setEffort(effortFromConfig);
}

Keep “ordering hazards” (e.g., user actions during resume vs backend load) scoped or explicitly disabled unless you can key off a reliable in-progress signal; otherwise treat them as separate follow-ups to avoid mixing multiple concurrency risks in one change.