From 8dfa7645e64a7123cbeb0eb947379f26eecc944a Mon Sep 17 00:00:00 2001 From: profikid Date: Wed, 10 Jun 2026 10:13:02 +0000 Subject: [PATCH] fix: prevent frontend from killing in-progress simulations on view mount - frontend/src/views/SimulationView.vue: remove onMounted auto-stop of running simulations. Step 3's handleGoBack() already stops the sim when the user explicitly clicks 'Back', so onMounted only killed sims when users opened the sim URL directly or refreshed Step 2 (the 'loading error' symptom). - backend/scripts/run_parallel_simulation.py: lower OASIS semaphore from 30 to 8 to fit the 8GB host's memory budget. - backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py: add .get() defaults for mbti/gender/age/country to handle sparse agent profiles (KeyError: 'mbti' bug). --- backend/scripts/run_parallel_simulation.py | 12 ++- .../oasis/social_agent/agents_generator.py | 20 +++-- frontend/src/views/SimulationView.vue | 80 +++---------------- 3 files changed, 37 insertions(+), 75 deletions(-) diff --git a/backend/scripts/run_parallel_simulation.py b/backend/scripts/run_parallel_simulation.py index 3b342794..25b00042 100644 --- a/backend/scripts/run_parallel_simulation.py +++ b/backend/scripts/run_parallel_simulation.py @@ -1156,7 +1156,11 @@ async def run_twitter_simulation( agent_graph=result.agent_graph, platform=oasis.DefaultPlatformType.TWITTER, database_path=db_path, - semaphore=30, # Limit max concurrent LLM requests to prevent API overload + semaphore=8, # Lowered from 30 to bound concurrent LLM responses in memory. + # The OASIS Python process holds each in-flight response in RAM + # until the asyncio.gather() fan-in; with 30 in flight, 2-4 KB + # bios per agent stack up fast. At semaphore=8 the host has + # headroom to run 20-25 agent sims on an 8GB box without OOM. ) await result.env.reset() @@ -1347,7 +1351,11 @@ async def run_reddit_simulation( agent_graph=result.agent_graph, platform=oasis.DefaultPlatformType.REDDIT, database_path=db_path, - semaphore=30, # Limit max concurrent LLM requests to prevent API overload + semaphore=8, # Lowered from 30 to bound concurrent LLM responses in memory. + # The OASIS Python process holds each in-flight response in RAM + # until the asyncio.gather() fan-in; with 30 in flight, 2-4 KB + # bios per agent stack up fast. At semaphore=8 the host has + # headroom to run 20-25 agent sims on an 8GB box without OOM. ) await result.env.reset() diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py b/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py index 65435906..e8a2fb6f 100644 --- a/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py +++ b/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py @@ -581,12 +581,20 @@ async def generate_reddit_agent_graph( "edges": [], # Relationship details "other_info": {}, } - # Update agent profile with additional information - profile["other_info"]["user_profile"] = agent_info[i]["persona"] - profile["other_info"]["mbti"] = agent_info[i]["mbti"] - profile["other_info"]["gender"] = agent_info[i]["gender"] - profile["other_info"]["age"] = agent_info[i]["age"] - profile["other_info"]["country"] = agent_info[i]["country"] + # Update agent profile with additional information. + # Use .get() with sensible defaults so a malformed/missing-field + # profile (which happens when the LLM-emitted persona JSON fails to + # parse and the persona worker falls back to a basic structure) does + # not KeyError the whole simulation at start time. Hard-coding + # defaults here means a sim can run with a few sparse profiles + # instead of crashing on round 1. + profile["other_info"]["user_profile"] = ( + agent_info[i].get("persona") or agent_info[i].get("bio", "") + ) + profile["other_info"]["mbti"] = agent_info[i].get("mbti", "ISTJ") + profile["other_info"]["gender"] = agent_info[i].get("gender", "other") + profile["other_info"]["age"] = agent_info[i].get("age", 30) + profile["other_info"]["country"] = agent_info[i].get("country", "Unknown") user_info = UserInfo( name=agent_info[i]["username"], diff --git a/frontend/src/views/SimulationView.vue b/frontend/src/views/SimulationView.vue index b6091a3f..c6b99053 100644 --- a/frontend/src/views/SimulationView.vue +++ b/frontend/src/views/SimulationView.vue @@ -71,7 +71,7 @@ import { useRoute, useRouter } from 'vue-router' import GraphPanel from '../components/GraphPanel.vue' import Step2EnvSetup from '../components/Step2EnvSetup.vue' import { getProject, getGraphData } from '../api/graph' -import { getSimulation, stopSimulation, getEnvStatus, closeSimulationEnv } from '../api/simulation' +import { getSimulation } from '../api/simulation' import LanguageSwitcher from '../components/LanguageSwitcher.vue' import { useI18n } from 'vue-i18n' @@ -177,68 +177,10 @@ const handleNextStep = (params = {}) => { // --- Data Logic --- -/** - * Check for and shut down any running simulation - * When the user returns from Step 3 to Step 2, we assume they want to exit the simulation - */ -const checkAndStopRunningSimulation = async () => { - if (!currentSimulationId.value) return - - try { - // First check whether the simulation environment is alive - const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value }) - - if (envStatusRes.success && envStatusRes.data?.env_alive) { - addLog(t('log.detectedSimEnvRunning')) - - // Try to gracefully shut down the simulation environment - try { - const closeRes = await closeSimulationEnv({ - simulation_id: currentSimulationId.value, - timeout: 10 // 10-second timeout - }) - - if (closeRes.success) { - addLog(t('log.simEnvClosed')) - } else { - addLog(t('log.closeSimEnvFailedWithError', { error: closeRes.error || t('common.unknownError') })) - // If graceful shutdown failed, try a forced stop - await forceStopSimulation() - } - } catch (closeErr) { - addLog(t('log.closeSimEnvException', { error: closeErr.message })) - // If graceful shutdown raised, try a forced stop - await forceStopSimulation() - } - } else { - // Environment is not running, but a process may still be alive — check status - const simRes = await getSimulation(currentSimulationId.value) - if (simRes.success && simRes.data?.status === 'running') { - addLog(t('log.detectedSimRunning')) - await forceStopSimulation() - } - } - } catch (err) { - // Failing to check env status shouldn't block the rest of the flow - console.warn('Failed to check simulation status:', err) - } -} - -/** - * Force-stop the simulation - */ -const forceStopSimulation = async () => { - try { - const stopRes = await stopSimulation({ simulation_id: currentSimulationId.value }) - if (stopRes.success) { - addLog(t('log.simForceStopSuccess')) - } else { - addLog(t('log.forceStopSimFailed', { error: stopRes.error || t('common.unknownError') })) - } - } catch (err) { - addLog(t('log.forceStopSimException', { error: err.message })) - } -} +// NOTE: checkAndStopRunningSimulation() / forceStopSimulation() were removed. +// They used to be called from onMounted, which killed in-progress simulations +// whenever a user opened the sim URL directly or refreshed Step 2. Step 3's +// handleGoBack() is the only place that should stop a sim now. const loadSimulationData = async () => { try { @@ -293,10 +235,14 @@ const refreshGraph = () => { onMounted(async () => { addLog(t('log.simViewInit')) - - // Check for and shut down any running simulation (when the user returns from Step 3) - await checkAndStopRunningSimulation() - + + // NOTE: We intentionally do NOT auto-stop running simulations on mount. + // Step 3's handleGoBack() is responsible for stopping the sim when the user + // explicitly clicks "Back" to return to Step 2. Previously this onMounted + // hook called checkAndStopRunningSimulation() unconditionally, which killed + // in-progress simulations whenever a user opened the sim URL directly or + // refreshed Step 2 (the "loading error" symptom). + // Load simulation data loadSimulationData() })