fix(simulation): stop discarding an existing run when Step 3 remounts

Step3Simulation mounted with a simulation id and immediately called
doStartSimulation() with force: true. Because the id lives in the route
path (/simulation/:simulationId/start), a browser refresh remounted the
component and re-sent that request, so the backend stopped the running
subprocess and cleaned the run directory: run_state.json, the twitter and
reddit actions.jsonl action logs, and both *_simulation.db files. The
timeline, posts and comments views read those files directly, so the data
was unrecoverable.

Probe the run status before deciding what to do. A simulation that has
never run (runner_status "idle" with no recorded actions) still starts
exactly as before. Anything else is adopted instead of restarted: live
runs resume polling, finished runs load their existing timeline read-only.
If the status probe itself fails we now do nothing rather than risk
overwriting a run we could not verify.

force is also no longer hardcoded to true. The remaining call site only
runs when there is nothing to lose, and the flag should never be an
implicit default.
This commit is contained in:
MrBeanDev 2026-08-04 07:03:40 +05:30
parent b5b53acc57
commit 5da9cc3d76
3 changed files with 66 additions and 2 deletions

View File

@ -398,7 +398,9 @@ const doStartSimulation = async () => {
const params = {
simulation_id: props.simulationId,
platform: 'parallel',
force: true, //
//
// force=true actions.jsonl *_simulation.db
force: false,
enable_graph_memory_update: true //
}
@ -437,6 +439,62 @@ const doStartSimulation = async () => {
}
}
//
// run_state runner_status='idle'
const hasRecoverableRun = (status) => {
if (!status) return false
if (status.runner_status && status.runner_status !== 'idle') return true
return (status.total_actions_count || 0) > 0
}
//
//
const resumeOrStartSimulation = async () => {
if (!props.simulationId) {
addLog(t('log.errorMissingSimId'))
return
}
let status = null
try {
const res = await getRunStatus(props.simulationId)
if (!res.success || !res.data) {
throw new Error(res.error || t('common.unknownError'))
}
status = res.data
} catch (err) {
//
startError.value = err.message
addLog(t('log.resumeCheckFailed', { error: err.message }))
emit('update-status', 'error')
return
}
if (!hasRecoverableRun(status)) {
await doStartSimulation()
return
}
//
runStatus.value = status
prevTwitterRound.value = status.twitter_current_round || 0
prevRedditRound.value = status.reddit_current_round || 0
await fetchRunStatusDetail()
const isLive = ['starting', 'running', 'paused', 'stopping'].includes(status.runner_status)
if (isLive) {
phase.value = 1
addLog(t('log.resumedRunningSim', { round: status.current_round || 0 }))
emit('update-status', 'processing')
startStatusPolling()
startDetailPolling()
} else {
phase.value = 2
addLog(t('log.loadedExistingSim', { actions: status.total_actions_count || 0 }))
emit('update-status', status.runner_status === 'failed' ? 'error' : 'completed')
}
}
//
const handleStopSimulation = async () => {
if (!props.simulationId) return
@ -691,7 +749,7 @@ watch(() => props.systemLogs?.length, () => {
onMounted(() => {
addLog(t('log.step3Init'))
if (props.simulationId) {
doStartSimulation()
resumeOrStartSimulation()
}
})

View File

@ -525,6 +525,9 @@
"loadConfigFailed": "Failed to load config: {error}",
"step2Init": "Step 2 environment setup initialized",
"step3Init": "Step 3 simulation run initialized",
"resumeCheckFailed": "✗ Could not read run status: {error}. Not starting, to avoid discarding an existing run.",
"resumedRunningSim": "↻ Reattached to the running simulation (round {round}) — not restarted",
"loadedExistingSim": "↻ Loaded the existing simulation run ({actions} events) — not restarted",
"startingDualSim": "Starting dual-platform parallel simulation...",
"setMaxRounds": "Max simulation rounds set to: {rounds}",
"graphMemoryUpdateEnabled": "Dynamic graph memory update enabled",

View File

@ -525,6 +525,9 @@
"loadConfigFailed": "加载配置失败: {error}",
"step2Init": "Step2 环境搭建初始化",
"step3Init": "Step3 模拟运行初始化",
"resumeCheckFailed": "✗ 无法获取运行状态: {error}。已跳过启动,避免丢弃已有的模拟数据。",
"resumedRunningSim": "↻ 已接管正在运行的模拟(第 {round} 轮)——未重新开始",
"loadedExistingSim": "↻ 已载入既有的模拟运行({actions} 条事件)——未重新开始",
"startingDualSim": "正在启动双平台并行模拟...",
"setMaxRounds": "设置最大模拟轮数: {rounds}",
"graphMemoryUpdateEnabled": "已开启动态图谱更新模式",