← 返回首页
fix: use atomic stage+iteration update on quality gate new-iteration … · Sibyl-Research-Team/AutoResearch-SibylSystem@2e8b3a3 · GitHub
Skip to content

Navigation Menu

Toggle navigation
Sign in
Appearance settings
Search or jump to...

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Include my email address so I can be contacted

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Resetting focus

Commit 2e8b3a3

Browse files
fix: use atomic stage+iteration update on quality gate new-iteration path
_get_next_stage now returns (next_stage, new_iteration) tuple. record_result uses update_stage_and_iteration() for atomic writes when iteration changes, eliminating the TOCTOU window where a crash between separate update_iteration and update_stage calls could leave status.json in an inconsistent state (iteration incremented but stage still at quality_gate).
1 parent b29bb23 commit 2e8b3a3

1 file changed

Lines changed: 21 additions & 15 deletions

File tree

‎sibyl/orchestrate.py‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,11 @@ def record_result(self, stage: str, result: str = "",
197197
if stage == "reflection":
198198
self._post_reflection_hook()
199199

200-
next_stage = self._get_next_stage(stage, result, score)
201-
self.ws.update_stage(next_stage)
200+
next_stage, new_iteration = self._get_next_stage(stage, result, score)
201+
if new_iteration is not None:
202+
self.ws.update_stage_and_iteration(next_stage, new_iteration)
203+
else:
204+
self.ws.update_stage(next_stage)
202205

203206
if score is not None:
204207
self.ws.write_file(
@@ -740,8 +743,12 @@ def _parse_quality_gate_params(self) -> tuple[float, float, int]:
740743
return score, threshold, max_iters
741744

742745
def _get_next_stage(self, current_stage: str, result: str = "",
743-
score: float | None = None) -> str:
744-
"""Determine the next stage based on current stage and result."""
746+
score: float | None = None) -> tuple[str, int | None]:
747+
"""Determine the next stage based on current stage and result.
748+
749+
Returns (next_stage, new_iteration). new_iteration is non-None only
750+
when the quality gate loops back for a new iteration.
751+
"""
745752
# experiment_decision: PIVOT loops back to idea_debate
746753
if current_stage == "experiment_decision":
747754
decision = self.ws.read_file("supervisor/experiment_analysis.md")
@@ -756,7 +763,7 @@ def _get_next_stage(self, current_stage: str, result: str = "",
756763
f"logs/idea_exp_cycle_{cycle + 1}.marker",
757764
f"PIVOT at iteration {iteration}",
758765
)
759-
return "idea_debate"
766+
return ("idea_debate", None)
760767
else:
761768
self.ws.add_error(
762769
f"PIVOT requested but cycle limit reached ({cycle}/{self.config.idea_exp_cycles})"
@@ -782,15 +789,15 @@ def _get_next_stage(self, current_stage: str, result: str = "",
782789
f"writing/critique/revision_round_{revision_rounds + 1}.marker",
783790
f"Revision round {revision_rounds + 1}, score={review_score}",
784791
)
785-
return "writing_integrate"
792+
return ("writing_integrate", None)
786793

787794
# init is transient: always advance to literature_search
788795
if current_stage == "init":
789-
return "literature_search"
796+
return ("literature_search", None)
790797

791798
# lark stages: skip if lark disabled
792799
if current_stage == "reflection" and not self.config.lark_enabled:
793-
return "quality_gate"
800+
return ("quality_gate", None)
794801

795802
# quality_gate: execute side effects and determine next stage
796803
if current_stage == "quality_gate":
@@ -804,7 +811,7 @@ def _get_next_stage(self, current_stage: str, result: str = "",
804811
if self.config.evolution_enabled:
805812
from sibyl.evolution import EvolutionEngine
806813
EvolutionEngine().run_cross_project_evolution()
807-
return "done"
814+
return ("done", None)
808815
else:
809816
# Tag end of iteration, archive artifacts, advance counter
810817
self.ws.git_tag(
@@ -817,18 +824,17 @@ def _get_next_stage(self, current_stage: str, result: str = "",
817824
self.ws.add_error(f"Archive failed for iteration {iteration}: {e}")
818825
# Clear stale artifacts that would pollute the next iteration
819826
self._clear_iteration_artifacts()
820-
# Update iteration counter; stage will be set by record_result
821-
self.ws.update_iteration(iteration + 1)
822-
return "literature_search" # start new iteration
827+
# Iteration counter update is atomic with stage in record_result
828+
return ("literature_search", iteration + 1)
823829

824830
try:
825831
idx = self.STAGES.index(current_stage)
826832
if idx + 1 < len(self.STAGES):
827-
return self.STAGES[idx + 1]
833+
return (self.STAGES[idx + 1], None)
828834
except ValueError:
829835
self.ws.add_error(f"Unknown stage '{current_stage}', forcing done")
830-
return "done"
831-
return current_stage
836+
return ("done", None)
837+
return (current_stage, None)
832838

833839
def _clear_iteration_artifacts(self):
834840
"""Clear stale working-directory artifacts between iterations.

0 commit comments

Comments
 (0)

Footer

© 2026 GitHub, Inc.