Skip to content

Commit 2e55f43

Browse files
authored
MOTO v1.0.8 Bug Fix
MOTO v1.0.8 Bug Fix #Bug Fix - Prevent cleared autonomous sessions from resuming while preserving completed run history.
1 parent 086efd8 commit 2e55f43

7 files changed

Lines changed: 144 additions & 26 deletions

File tree

.cursor/rules/part-3-autonomous-research-mode.mdc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ This file persists the current workflow state to enable **automatic resume** aft
13181318
- Before completed-paper proof verification (`paper_phase="paper_proof_verification"`)
13191319
- **During Tier 3 final answer generation phases**
13201320

1321-
On **clean stop** (user-initiated via stop button), this file is preserved for pause/resume. Only `clear_all_data()` should clear workflow state. `_save_workflow_state()` must preserve the previous `paper_phase` when called without an explicit phase, and only clear the phase when passed `phase=None` intentionally after successful completion.
1321+
On **clean stop** (user-initiated via stop button), this file is preserved for pause/resume. Only `clear_all_data()` should clear workflow state. `clear_all_data()` preserves completed session files for history, marks existing sessions non-resumable/history-only, clears pending child-aggregator queue state, and resets live memory path bindings so the next Start creates a fresh session. `_save_workflow_state()` must preserve the previous `paper_phase` when called without an explicit phase, and only clear the phase when passed `phase=None` intentionally after successful completion.
13221322

13231323
On **restart/crash recovery**, if this file exists with a resumable tier/topic/paper (regardless of `is_running`), the system detects an interrupted workflow and:
13241324
1. Restores internal state (topic ID, acceptance counts, model config, etc.)
@@ -1327,14 +1327,14 @@ On **restart/crash recovery**, if this file exists with a resumable tier/topic/p
13271327
4. Detects completed papers paused before proof verification and resumes `paper_proof_verification` before moving on
13281328
5. Broadcasts `auto_research_resumed` WebSocket event
13291329

1330-
If `workflow_state.json` is stale, idle, or missing, session recovery must conservatively synthesize a resume point from durable `session_stats.json`, brainstorm metadata/database files, and in-progress paper metadata/content. This includes scanning `papers/*_metadata.json` for `status="in_progress"` when stats lost `current_paper_id`; the resume phase is detected from saved paper content rather than defaulting to body.
1330+
If `workflow_state.json` is stale, idle, or missing, session recovery must conservatively synthesize a resume point from durable `session_stats.json`, brainstorm metadata/database files, and in-progress paper metadata/content unless the session metadata is marked non-resumable/history-only. This includes scanning `papers/*_metadata.json` for `status="in_progress"` when stats lost `current_paper_id`; the resume phase is detected from saved paper content rather than defaulting to body.
13311331

13321332

13331333
**Important Notes:**
13341334
- The user research prompt is saved in `auto_research_metadata.json`, not the workflow state
13351335
- Model configuration is saved to allow resuming with the same model settings
1336-
- If the workflow state file is corrupted or missing, first try durable session-file recovery; start fresh only if no current topic, in-progress paper, completed unpapered brainstorm, completed papers, or active Tier 3 state can be recovered
1337-
- The `clear_all_data` API endpoint clears the workflow state along with all other data
1336+
- If the workflow state file is corrupted or missing, first try durable session-file recovery; start fresh only if no current topic, in-progress paper, completed unpapered brainstorm, completed papers, or active Tier 3 state can be recovered, and only when the session is not marked non-resumable/history-only
1337+
- The `clear_all_data` API endpoint preserves session files for history, marks sessions `resume_disabled=true` / `status="cleared"`, and must fail if any session cannot be marked non-resumable
13381338

13391339
---
13401340

backend/autonomous/core/autonomous_coordinator.py

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6646,9 +6646,11 @@ async def clear_all_data(self) -> None:
66466646
if self._running or self._state.is_running:
66476647
raise RuntimeError("Cannot clear data while running")
66486648

6649+
import json
66496650
import shutil
66506651
import time
66516652
from pathlib import Path
6653+
from backend.aggregator.core.queue_manager import queue_manager
66526654

66536655
# Wait briefly for any pending async file operations to complete
66546656
await asyncio.sleep(0.3)
@@ -6677,12 +6679,57 @@ def safe_rmtree(path: Path, max_retries: int = 5) -> bool:
66776679
raise
66786680
return False
66796681

6680-
# Step 0: Clear all session workflow states (prevents resume from old sessions)
6682+
# Step 0: Make existing sessions history-only so completed work stays
6683+
# browsable but durable recovery will not restart it as live work.
66816684
try:
66826685
sessions_dir = Path(system_config.auto_sessions_base_dir)
6686+
cleared_session_count = 0
6687+
session_mark_failures = []
66836688
if sessions_dir.exists():
66846689
for session_dir in sessions_dir.iterdir():
66856690
if session_dir.is_dir():
6691+
now = datetime.now().isoformat()
6692+
metadata_path = session_dir / "session_metadata.json"
6693+
metadata = {}
6694+
if metadata_path.exists():
6695+
try:
6696+
async with aiofiles.open(metadata_path, 'r', encoding='utf-8') as f:
6697+
raw_metadata = await f.read()
6698+
metadata = json.loads(raw_metadata) if raw_metadata.strip() else {}
6699+
except Exception as e:
6700+
logger.warning(f"Could not read session metadata for {session_dir.name}: {e}")
6701+
6702+
metadata.setdefault("session_id", session_dir.name)
6703+
if not metadata.get("user_prompt") and metadata.get("user_research_prompt"):
6704+
metadata["user_prompt"] = metadata.get("user_research_prompt")
6705+
metadata["status"] = "cleared"
6706+
metadata["resume_disabled"] = True
6707+
metadata["cleared_at"] = now
6708+
metadata["last_updated"] = now
6709+
6710+
try:
6711+
async with aiofiles.open(metadata_path, 'w', encoding='utf-8') as f:
6712+
await f.write(json.dumps(metadata, indent=2))
6713+
cleared_session_count += 1
6714+
except Exception as e:
6715+
message = f"Could not mark session as cleared for {session_dir.name}: {e}"
6716+
session_mark_failures.append(message)
6717+
logger.error(message)
6718+
6719+
stats_path = session_dir / "session_stats.json"
6720+
if stats_path.exists():
6721+
try:
6722+
async with aiofiles.open(stats_path, 'r', encoding='utf-8') as f:
6723+
raw_stats = await f.read()
6724+
stats = json.loads(raw_stats) if raw_stats.strip() else {}
6725+
stats["current_brainstorm_id"] = None
6726+
stats["current_paper_id"] = None
6727+
stats["last_updated"] = now
6728+
async with aiofiles.open(stats_path, 'w', encoding='utf-8') as f:
6729+
await f.write(json.dumps(stats, indent=2))
6730+
except Exception as e:
6731+
logger.warning(f"Could not clear active stats for {session_dir.name}: {e}")
6732+
66866733
workflow_state_file = session_dir / "workflow_state.json"
66876734
if workflow_state_file.exists():
66886735
try:
@@ -6691,10 +6738,33 @@ def safe_rmtree(path: Path, max_retries: int = 5) -> bool:
66916738
except Exception as e:
66926739
# Non-critical: workflow state files are small
66936740
logger.warning(f"Could not clear workflow state for {session_dir.name}: {e}")
6694-
logger.info("Cleared all session workflow states")
6741+
if session_mark_failures:
6742+
critical_errors.append(
6743+
"Failed to mark one or more sessions non-resumable: "
6744+
+ "; ".join(session_mark_failures)
6745+
)
6746+
else:
6747+
successes.append(f"Marked {cleared_session_count} session(s) as history-only")
6748+
logger.info("Marked session histories as non-resumable and cleared workflow states")
66956749
except Exception as e:
6696-
errors.append(f"Failed to clear session workflow states: {e}")
6697-
logger.error(errors[-1])
6750+
critical_errors.append(f"Failed to mark sessions history-only: {e}")
6751+
logger.error(critical_errors[-1])
6752+
6753+
# Step 0b: Reset live path bindings before clearing legacy state.
6754+
# Session files remain as history; current Stage 1/2 views should read
6755+
# from the empty legacy roots until the next Start creates a new session.
6756+
try:
6757+
await session_manager.clear()
6758+
brainstorm_memory.set_session_manager(None)
6759+
paper_library.set_session_manager(None)
6760+
research_metadata.set_session_manager(None)
6761+
final_answer_memory.set_session_manager(None)
6762+
proof_database.set_session_manager(None)
6763+
successes.append("Reset live session path bindings")
6764+
logger.info("Reset live session path bindings after clear")
6765+
except Exception as e:
6766+
errors.append(f"Failed to reset live session path bindings: {e}")
6767+
logger.warning(errors[-1])
66986768

66996769
# Step 1: Clear brainstorms directory
67006770
try:
@@ -6774,6 +6844,15 @@ def safe_rmtree(path: Path, max_retries: int = 5) -> bool:
67746844
# Critical: RAG state affects future operations
67756845
critical_errors.append(f"Failed to clear RAG state: {e}")
67766846
logger.error(critical_errors[-1])
6847+
6848+
# Step 7b: Clear any queued submissions left by cancelled child aggregators.
6849+
try:
6850+
await queue_manager.clear()
6851+
successes.append("Cleared pending submission queue")
6852+
logger.info("Cleared pending submission queue")
6853+
except Exception as e:
6854+
errors.append(f"Failed to clear pending submission queue: {e}")
6855+
logger.warning(errors[-1])
67776856

67786857
# Step 8: Reset internal state
67796858
self._current_topic_id = None
@@ -6801,16 +6880,6 @@ def safe_rmtree(path: Path, max_retries: int = 5) -> bool:
68016880
# Step 9: Reset state object
68026881
self._state = AutonomousResearchState()
68036882

6804-
# Step 10: Clear session manager state
6805-
try:
6806-
await session_manager.clear()
6807-
successes.append("Cleared session manager state")
6808-
logger.info("Cleared session manager state")
6809-
except Exception as e:
6810-
# Non-critical: session manager will reset on next start
6811-
errors.append(f"Failed to clear session manager: {e}")
6812-
logger.warning(errors[-1])
6813-
68146883
# Report results with graceful degradation
68156884
success_count = len(successes)
68166885
error_count = len(errors)

backend/autonomous/memory/brainstorm_memory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def set_session_manager(self, session_manager) -> None:
3939
if session_manager and session_manager.is_session_active:
4040
self._base_dir = session_manager.get_brainstorms_dir()
4141
logger.info(f"Brainstorm memory using session path: {self._base_dir}")
42+
else:
43+
self._base_dir = Path(system_config.auto_brainstorms_dir)
44+
logger.info(f"Brainstorm memory using legacy path: {self._base_dir}")
4245

4346
async def initialize(self) -> None:
4447
"""Initialize the brainstorm memory directory."""

backend/autonomous/memory/final_answer_memory.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ def set_session_manager(self, session_manager) -> None:
178178
self._rejections_path = self._base_dir / "tier3_rejections.txt"
179179
self._final_volume_path = self._base_dir / "final_volume.txt"
180180
logger.info(f"Final answer memory using session path: {self._base_dir}")
181+
else:
182+
self._base_dir = Path(system_config.data_dir) / "auto_final_answer"
183+
self._state_path = self._base_dir / "final_answer_state.json"
184+
self._volume_path = self._base_dir / "volume_organization.json"
185+
self._rejections_path = self._base_dir / "tier3_rejections.txt"
186+
self._final_volume_path = self._base_dir / "final_volume.txt"
187+
logger.info(f"Final answer memory using legacy path: {self._base_dir}")
188+
189+
self._state = None
181190

182191
async def initialize(self) -> None:
183192
"""Initialize the final answer memory directories and load state."""

backend/autonomous/memory/paper_library.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def set_session_manager(self, session_manager) -> None:
4848
self._archive_dir = session_manager.get_papers_dir() / "archive"
4949
self._pruned_dir = session_manager.get_papers_dir() / "pruned"
5050
logger.info("Paper library using session path: %s", redact_log_text(self._base_dir, 240))
51+
else:
52+
self._base_dir = Path(system_config.auto_papers_dir)
53+
self._archive_dir = Path(system_config.auto_papers_archive_dir)
54+
self._pruned_dir = self._base_dir / "pruned"
55+
logger.info("Paper library using legacy path: %s", redact_log_text(self._base_dir, 240))
5156

5257
async def initialize(self) -> None:
5358
"""Initialize the paper library directories."""

backend/autonomous/memory/research_metadata.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ def set_session_manager(self, session_manager) -> None:
5050
self._stats_path = session_path / "session_stats.json"
5151
self._workflow_state_path = session_path / "workflow_state.json"
5252
logger.info(f"Research metadata using session path: {session_path}")
53+
else:
54+
self._metadata_path = Path(system_config.auto_research_metadata_file)
55+
self._stats_path = Path(system_config.auto_research_stats_file)
56+
self._workflow_state_path = Path(system_config.auto_workflow_state_file)
57+
logger.info("Research metadata using legacy paths")
58+
59+
self._data = None
60+
self._stats = None
61+
self._workflow_state = None
5362

5463
def _get_default_stats(self) -> Dict[str, Any]:
5564
"""Default statistics structure."""

backend/autonomous/memory/session_manager.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
logger = logging.getLogger(__name__)
2222

2323

24+
NON_RESUMABLE_SESSION_STATUSES = {"cleared", "history_only", "archived", "complete"}
25+
26+
2427
def _session_paper_has_section(content: str, section_name: str) -> bool:
2528
base_patterns = [
2629
rf"##\s*{section_name}",
@@ -230,6 +233,17 @@ async def resume_session(self, session_id: str, base_dir: Optional[str] = None)
230233
if metadata_path.exists():
231234
async with aiofiles.open(metadata_path, 'r', encoding='utf-8') as f:
232235
metadata = json.loads(await f.read())
236+
session_status = str(metadata.get("status", "")).lower()
237+
if metadata.get("resume_disabled") or session_status in NON_RESUMABLE_SESSION_STATUSES:
238+
logger.error(
239+
"Refusing to resume non-resumable session: %s (status=%s)",
240+
session_id,
241+
session_status or "unknown",
242+
)
243+
self._session_path = None
244+
self._user_prompt = None
245+
self._session_id = None
246+
return None
233247
self._user_prompt = metadata.get("user_prompt", "")
234248
self._session_id = metadata.get("session_id", session_id)
235249
else:
@@ -345,7 +359,24 @@ async def find_interrupted_session(self, base_dir: Optional[str] = None) -> Opti
345359

346360
workflow_state_path = session_dir / "workflow_state.json"
347361
workflow_state = None
362+
session_metadata = {}
363+
user_prompt = ""
348364
try:
365+
session_metadata_path = session_dir / "session_metadata.json"
366+
if session_metadata_path.exists():
367+
async with aiofiles.open(session_metadata_path, 'r', encoding='utf-8') as f:
368+
session_metadata = json.loads(await f.read())
369+
user_prompt = session_metadata.get("user_prompt", "") or session_metadata.get("user_research_prompt", "")
370+
371+
session_status = str(session_metadata.get("status", "")).lower()
372+
if session_metadata.get("resume_disabled") or session_status in NON_RESUMABLE_SESSION_STATUSES:
373+
logger.debug(
374+
"Skipping non-resumable session %s (status=%s)",
375+
session_dir.name,
376+
session_status or "unknown",
377+
)
378+
continue
379+
349380
if workflow_state_path.exists():
350381
async with aiofiles.open(workflow_state_path, 'r', encoding='utf-8') as f:
351382
raw = await f.read()
@@ -371,14 +402,6 @@ async def find_interrupted_session(self, base_dir: Optional[str] = None) -> Opti
371402
continue
372403

373404
if has_tier and (has_topic or has_papers):
374-
# Load session metadata for user prompt
375-
session_metadata_path = session_dir / "session_metadata.json"
376-
user_prompt = ""
377-
if session_metadata_path.exists():
378-
async with aiofiles.open(session_metadata_path, 'r', encoding='utf-8') as f:
379-
session_metadata = json.loads(await f.read())
380-
user_prompt = session_metadata.get("user_prompt", "")
381-
382405
resumable_sessions.append({
383406
"session_id": session_dir.name,
384407
"path": str(session_dir),

0 commit comments

Comments
 (0)