625 lines
23 KiB
Python
625 lines
23 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, Flag, auto
|
|
from pathlib import Path
|
|
|
|
from band_aider.ai_client.openai_compatible_client import OpenAICompatibleClient
|
|
from band_aider.context.class_meta import ClassMeta
|
|
from band_aider.context.cot_task_generator import CotTaskGenerator, CotTaskRecord
|
|
from band_aider.context.contex_bag import ContextBag
|
|
from band_aider.context.working_backlog import (
|
|
DeferredRemediationHandoff,
|
|
FailedQAItem,
|
|
RemediationDeduplicationKey,
|
|
)
|
|
from band_aider.context.traversed_frontier_history import TraversedFrontierHistory
|
|
from band_aider.judgement.code_parser import CodeParser
|
|
from band_aider.judgement.file_manager import FileManager
|
|
from band_aider.judgement.intent_check import IntentCheckFactory
|
|
from band_aider.judgement.prompt_formatter import PromptFormatterBuilder
|
|
from band_aider.judgement.dependency_discovery_strategy import DependencyTraversalCache
|
|
|
|
|
|
_UNATTRIBUTED_REMEDIATION_LOCATION = "[unattributed deferred remediation]"
|
|
|
|
|
|
class Intent(Flag):
|
|
"""Intent identifies the broad mode the global loop is trying to execute."""
|
|
|
|
FEATURE = auto()
|
|
REFACTOR = auto()
|
|
|
|
|
|
class SovereignState(str, Enum):
|
|
"""Durable outer-loop states used by story and Supreme coordination."""
|
|
|
|
SUPREME_ACTIVE = "supreme_active"
|
|
BETWEEN_STORIES = "between_stories"
|
|
SUPREME_FINISHED_UNREPORTED = "supreme_finished_unreported"
|
|
COMPLETED = "completed"
|
|
|
|
@classmethod
|
|
def coerce(
|
|
cls,
|
|
value: object,
|
|
default: "SovereignState",
|
|
) -> "SovereignState":
|
|
"""Convert the serialized enum value, using the supplied current-state default."""
|
|
|
|
if isinstance(value, cls):
|
|
return value
|
|
try:
|
|
return cls(str(value))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
@dataclass
|
|
class TaskCursor:
|
|
"""Durable state for the currently active COT task and its stabilization work."""
|
|
|
|
current_cot_index: int = 0
|
|
active_root: ClassMeta | None = None
|
|
visited_locations_by_cot: dict[str, set[str]] = field(default_factory=dict)
|
|
stabilized_locations_by_cot: dict[str, set[str]] = field(default_factory=dict)
|
|
marginal_result_start: int = 0
|
|
active_stabilization_key: str | None = None
|
|
deferred_remediation_by_cot: dict[str, list[FailedQAItem]] = field(
|
|
default_factory=dict
|
|
)
|
|
prior_traversed_frontiers: TraversedFrontierHistory | None = None
|
|
|
|
|
|
@dataclass
|
|
class StoryCursor:
|
|
"""Durable state for the active story and all of its recursive work."""
|
|
|
|
current_story_index: int = 0
|
|
active_story: str = ""
|
|
sovereign_phase: SovereignState = SovereignState.SUPREME_ACTIVE
|
|
search_policy_scope: str = "broad"
|
|
search_policy_reason: str = ""
|
|
cot_task_records: list[CotTaskRecord] = field(default_factory=list)
|
|
outer_iteration: int = 0
|
|
marginal_result: list[str] = field(default_factory=list)
|
|
stabilization_summaries_by_cot: dict[str, list[str]] = field(default_factory=dict)
|
|
run_summaries: list[str] = field(default_factory=list)
|
|
claimed_locations_this_recursive_pass: set[str] = field(default_factory=set)
|
|
bootstrap_involved_locations: set[str] = field(default_factory=set)
|
|
_stabilization_started: bool = False
|
|
_bootstrap_started: bool = False
|
|
_bootstrap_completed: bool = False
|
|
task_cursor: TaskCursor = field(default_factory=TaskCursor)
|
|
|
|
def __getstate__(self) -> dict[str, object]:
|
|
"""Serialize enum state as its stable checkpoint value."""
|
|
|
|
state = dict(self.__dict__)
|
|
phase = state.get("sovereign_phase")
|
|
if isinstance(phase, SovereignState):
|
|
state["sovereign_phase"] = phase.value
|
|
return state
|
|
|
|
|
|
class GlobalContext(ContextBag):
|
|
"""GlobalContext owns top-level workflow memory and the FileManager lifecycle."""
|
|
|
|
parser: CodeParser
|
|
client: OpenAICompatibleClient
|
|
src_root: Path
|
|
repo_root: Path
|
|
test_root: Path
|
|
actor: str
|
|
prompt_formatter_builder: PromptFormatterBuilder
|
|
intent_check_factory: IntentCheckFactory
|
|
intent: Intent | None
|
|
original_prompt: str
|
|
search_anchors: tuple[str, ...]
|
|
parser_factory: object | None
|
|
logger: logging.Logger
|
|
stories: list[str]
|
|
supreme_completion_state: SovereignState
|
|
supreme_result: str
|
|
supreme_reported: bool
|
|
story_cursor: StoryCursor
|
|
relevant_files: list[ClassMeta]
|
|
_file_manager: FileManager | None
|
|
|
|
def __getstate__(self) -> dict[str, object]:
|
|
"""Return durable global context state while stripping runtime-owned collaborators."""
|
|
|
|
state = super().__getstate__()
|
|
state.pop("parser", None)
|
|
state.pop("client", None)
|
|
state.pop("intent_check_factory", None)
|
|
state.pop("prompt_formatter_builder", None)
|
|
state.pop("parser_factory", None)
|
|
completion_state = state.get("supreme_completion_state")
|
|
if isinstance(completion_state, SovereignState):
|
|
state["supreme_completion_state"] = completion_state.value
|
|
return state
|
|
|
|
def _create_file_manager(self) -> FileManager:
|
|
"""Create the owned file manager for this global context."""
|
|
|
|
fm = FileManager(
|
|
parser=self.parser,
|
|
client=self.client,
|
|
repo_root=self.repo_root,
|
|
src_root=self.src_root,
|
|
test_root=self.test_root,
|
|
actor=self.actor,
|
|
prompt_formatter_builder=self.prompt_formatter_builder,
|
|
prompt=self.goal,
|
|
repo_exec=self.parser.repo_exec,
|
|
parser_factory=self.parser_factory,
|
|
)
|
|
return fm
|
|
|
|
def _has_relevance(self, candidate: ClassMeta) -> bool:
|
|
"""Return whether a candidate has parser-discovered relevance metadata."""
|
|
|
|
return bool(candidate.relevance or candidate.relevant_symbols)
|
|
|
|
def _refresh_intent(self) -> None:
|
|
"""Populate intent from the intent check."""
|
|
|
|
intent_check = self.intent_check_factory.create(self.goal)
|
|
|
|
if intent_check(*self.relevant_files):
|
|
self.intent = Intent.FEATURE
|
|
return
|
|
|
|
self.intent = Intent.REFACTOR
|
|
|
|
def _refresh_relevant_files(self) -> None:
|
|
"""Populate relevant files from parser-discovered source metadata."""
|
|
|
|
self.relevant_files = []
|
|
|
|
for candidate in self.file_manager.src_classes:
|
|
if self._has_relevance(candidate):
|
|
self.relevant_files.append(candidate)
|
|
self.logger.info(
|
|
f"[probe] initial relevant sources={len(self.relevant_files)} "
|
|
f"of={len(self.file_manager.src_classes)}",
|
|
)
|
|
self.relevant_files = self.file_manager.expand_relevant_upstream(
|
|
list(self.relevant_files),
|
|
self.current_cot_task_record.task if self.current_cot_task_record else "",
|
|
)
|
|
self.logger.info(
|
|
f"[probe] expanded relevant sources={len(self.relevant_files)}",
|
|
)
|
|
|
|
def _cdc_evidence_summary(self) -> str:
|
|
"""Return compact CDC evidence from all current managed classes."""
|
|
|
|
evidence = []
|
|
|
|
for class_meta in self.classes:
|
|
for changed_at, actor, description in class_meta.cdc:
|
|
evidence.append(
|
|
f"{changed_at.isoformat()} {actor} {class_meta.location}: {description}"
|
|
)
|
|
|
|
return "\n".join(evidence)
|
|
|
|
@property
|
|
def classes(self) -> list[ClassMeta]:
|
|
"""Return current source and test metadata from the owned file manager."""
|
|
|
|
if self._file_manager is None:
|
|
return []
|
|
|
|
return [
|
|
*self._file_manager.src_classes,
|
|
*self._file_manager.test_classes,
|
|
]
|
|
|
|
@property
|
|
def current_cot_task_record(self) -> CotTaskRecord | None:
|
|
"""Return the currently active COT task record."""
|
|
|
|
if not self.story_cursor.cot_task_records:
|
|
return None
|
|
if self.story_cursor.task_cursor.current_cot_index >= len(self.story_cursor.cot_task_records):
|
|
return None
|
|
|
|
return self.story_cursor.cot_task_records[
|
|
self.story_cursor.task_cursor.current_cot_index
|
|
]
|
|
|
|
@property
|
|
def file_manager(self) -> FileManager:
|
|
"""Return the owned file manager."""
|
|
|
|
if self._file_manager is None:
|
|
self._file_manager = self._create_file_manager()
|
|
|
|
return self._file_manager
|
|
|
|
@property
|
|
def bootstrap_started(self) -> bool:
|
|
"""Return whether this checkpoint crossed into BootstrapLoop."""
|
|
|
|
return self.story_cursor._bootstrap_started
|
|
|
|
@bootstrap_started.setter
|
|
def bootstrap_started(self, value: bool) -> None:
|
|
"""Persist the visible bootstrap-started resume boundary when it changes."""
|
|
|
|
next_value = bool(value)
|
|
if self.story_cursor._bootstrap_started == next_value:
|
|
return
|
|
|
|
self.story_cursor._bootstrap_started = next_value
|
|
self.persist()
|
|
|
|
@property
|
|
def bootstrap_completed(self) -> bool:
|
|
"""Return whether bootstrap mapping completed for this recursive pass."""
|
|
|
|
return self.story_cursor._bootstrap_completed
|
|
|
|
@bootstrap_completed.setter
|
|
def bootstrap_completed(self, value: bool) -> None:
|
|
"""Persist the visible bootstrap-completed resume boundary when it changes."""
|
|
|
|
next_value = bool(value)
|
|
if self.story_cursor._bootstrap_completed == next_value:
|
|
return
|
|
|
|
self.story_cursor._bootstrap_completed = next_value
|
|
self.persist()
|
|
|
|
@property
|
|
def stabilization_started(self) -> bool:
|
|
"""Return whether this checkpoint crossed into StabilizationLoop."""
|
|
|
|
return self.story_cursor._stabilization_started
|
|
|
|
@stabilization_started.setter
|
|
def stabilization_started(self, value: bool) -> None:
|
|
"""Persist the visible SupremeLoop resume boundary when it changes."""
|
|
|
|
next_value = bool(value)
|
|
if self.story_cursor._stabilization_started == next_value:
|
|
return
|
|
|
|
self.story_cursor._stabilization_started = next_value
|
|
self.persist()
|
|
|
|
def __init__(
|
|
self,
|
|
goal: str,
|
|
parser: CodeParser,
|
|
client: OpenAICompatibleClient,
|
|
repo_root: Path,
|
|
src_root: Path,
|
|
test_root: Path,
|
|
actor: str,
|
|
intent_check_factory: IntentCheckFactory,
|
|
prompt_formatter_builder: PromptFormatterBuilder,
|
|
original_prompt: str | None = None,
|
|
search_anchors: tuple[str, ...] = (),
|
|
parser_factory: object | None = None,
|
|
search_policy_scope: str = "broad",
|
|
search_policy_reason: str = "",
|
|
repo_exec: object | None = None,
|
|
stories: list[str] | None = None,
|
|
current_story_index: int = 0,
|
|
active_story: str | None = None,
|
|
) -> None:
|
|
"""Initialize global workflow state and load the first metadata view."""
|
|
|
|
self.goal = goal
|
|
self.parser = parser
|
|
self.client = client
|
|
self.src_root = src_root
|
|
self.repo_root = repo_root
|
|
self.test_root = test_root
|
|
self.actor = actor
|
|
self.prompt_formatter_builder = prompt_formatter_builder
|
|
self.intent_check_factory = intent_check_factory
|
|
self.intent = None
|
|
self.original_prompt = original_prompt or goal
|
|
self.search_anchors = tuple(search_anchors)
|
|
self.parser_factory = parser_factory
|
|
self.stories = list(stories or [])
|
|
self.story_cursor = StoryCursor(
|
|
current_story_index=current_story_index,
|
|
active_story=active_story if active_story is not None else goal,
|
|
search_policy_scope=search_policy_scope,
|
|
search_policy_reason=search_policy_reason,
|
|
)
|
|
self.supreme_completion_state = SovereignState.SUPREME_ACTIVE
|
|
self.supreme_result = ""
|
|
self.supreme_reported = False
|
|
self._file_manager = None
|
|
self.relevant_files: list[ClassMeta] = []
|
|
|
|
def generate_cot_tasks(self) -> None:
|
|
"""Generate and dependency-order COT tasks for the current recursive pass."""
|
|
|
|
file_manager = self._file_manager
|
|
if file_manager is None:
|
|
raise RuntimeError(
|
|
"COT task breakdown requires an initialized FileManager."
|
|
)
|
|
|
|
task_cursor = self.story_cursor.task_cursor
|
|
task_cursor.current_cot_index = 0
|
|
task_cursor.visited_locations_by_cot.clear()
|
|
task_cursor.stabilized_locations_by_cot.clear()
|
|
task_cursor.active_root = None
|
|
task_cursor.active_stabilization_key = None
|
|
task_cursor.deferred_remediation_by_cot.clear()
|
|
task_cursor.prior_traversed_frontiers = None
|
|
|
|
records = CotTaskGenerator(
|
|
self.client,
|
|
file_manager=file_manager,
|
|
prompt_formatter_builder=self.prompt_formatter_builder,
|
|
).generate(
|
|
self.goal,
|
|
self.relevant_files,
|
|
)
|
|
self.story_cursor.cot_task_records = records
|
|
self.persist()
|
|
|
|
def record_bootstrap_involved_locations(self, locations: list[str]) -> None:
|
|
"""Persist bootstrap-touched source locations for first stabilization story seeding."""
|
|
|
|
self.story_cursor.bootstrap_involved_locations.update(
|
|
location
|
|
for location in (str(item).strip() for item in locations)
|
|
if location
|
|
)
|
|
self.persist()
|
|
|
|
def record_stabilization_result(
|
|
self,
|
|
files: list[ClassMeta],
|
|
summary: str,
|
|
) -> None:
|
|
"""Record stabilization output and attach it to current COT task history."""
|
|
|
|
self.story_cursor.marginal_result.append(summary)
|
|
|
|
record = self.current_cot_task_record
|
|
task = record.task if record else self.original_prompt
|
|
self.story_cursor.stabilization_summaries_by_cot.setdefault(task, []).append(summary)
|
|
|
|
for class_meta in files:
|
|
current = self.file_manager.current_src(class_meta.location)
|
|
if current is not None:
|
|
self.file_manager.upsert_src(current, summary)
|
|
|
|
def current_cot_key(self) -> str:
|
|
"""Return the persistence key for the active COT task."""
|
|
|
|
return str(self.story_cursor.task_cursor.current_cot_index)
|
|
|
|
def previous_cot_root_location_for_current_cot(self) -> str | None:
|
|
"""Return the immediately preceding completed COT root for readonly source evidence."""
|
|
|
|
current_index = self.story_cursor.task_cursor.current_cot_index
|
|
if current_index == 0:
|
|
return None
|
|
records = self.story_cursor.cot_task_records
|
|
return records[current_index - 1].root.location
|
|
|
|
def previous_cot_records_for_current_cot(self) -> tuple[CotTaskRecord, ...]:
|
|
"""Return every earlier COT record in its original stable order."""
|
|
|
|
current_index = self.story_cursor.task_cursor.current_cot_index
|
|
return tuple(self.story_cursor.cot_task_records[:current_index])
|
|
|
|
def future_cot_records_for_current_cot(self) -> list[CotTaskRecord]:
|
|
"""Return later planned COT records while Stabilization owns closure calculation."""
|
|
|
|
current_index = self.story_cursor.task_cursor.current_cot_index
|
|
return list(self.story_cursor.cot_task_records[current_index + 1:])
|
|
|
|
def inherited_deferred_remediation_for_current_cot(
|
|
self,
|
|
) -> dict[str, list[FailedQAItem]]:
|
|
"""Return live inherited remediation staged as deferred work for this COT."""
|
|
|
|
inherited: dict[str, list[FailedQAItem]] = {}
|
|
# Preserve one independently attributable item at its focused source location.
|
|
for item in self.story_cursor.task_cursor.deferred_remediation_by_cot.get(
|
|
self.current_cot_key(),
|
|
[],
|
|
):
|
|
source = item.focused_spotlight
|
|
# Ambiguous work cannot become silently runnable in a later COT.
|
|
if source is None:
|
|
inherited.setdefault(_UNATTRIBUTED_REMEDIATION_LOCATION, []).append(item)
|
|
continue
|
|
inherited.setdefault(source.location, []).append(item)
|
|
return inherited
|
|
|
|
def handoff_deferred_remediation_to_next_cot(
|
|
self,
|
|
handoff: DeferredRemediationHandoff,
|
|
) -> bool:
|
|
"""Durably relay deferred source remediation through the immediate next COT inbox."""
|
|
|
|
items = [*handoff.inherited_items, *handoff.deferred_items]
|
|
task_cursor = self.story_cursor.task_cursor
|
|
current_index = task_cursor.current_cot_index
|
|
next_index = current_index + 1
|
|
# Cross-COT transport preserves failed source remediation only; Beta testing stays local.
|
|
if any(not isinstance(item, FailedQAItem) for item in items):
|
|
return False
|
|
# Source attribution is mandatory before a live item can cross ownership boundaries.
|
|
if any(item.focused_spotlight is None for item in items):
|
|
return False
|
|
# Stabilization calls this boundary only after non-final frontier exhaustion.
|
|
assert items
|
|
assert next_index < len(self.story_cursor.cot_task_records)
|
|
|
|
next_key = str(next_index)
|
|
next_items = list(task_cursor.deferred_remediation_by_cot.get(next_key, []))
|
|
known = [
|
|
RemediationDeduplicationKey.from_item(item)
|
|
for item in next_items
|
|
]
|
|
# Preserve exact item objects and evidence while preventing resume/retry duplicates.
|
|
for item in items:
|
|
key = RemediationDeduplicationKey.from_item(item)
|
|
if key in known:
|
|
continue
|
|
known.append(key)
|
|
next_items.append(item)
|
|
# Persist next-COT work before the accepted current COT acknowledges its own inbox.
|
|
task_cursor.deferred_remediation_by_cot[next_key] = next_items
|
|
self.persist()
|
|
return True
|
|
|
|
def visited_locations_for_current_cot(self) -> set[str]:
|
|
"""Return selected/attempted root locations for the active COT task."""
|
|
|
|
return self.story_cursor.task_cursor.visited_locations_by_cot.setdefault(
|
|
self.current_cot_key(), set()
|
|
)
|
|
|
|
def stabilized_locations_for_current_cot(self) -> set[str]:
|
|
"""Return stabilized root locations for the active COT task."""
|
|
|
|
return self.story_cursor.task_cursor.stabilized_locations_by_cot.setdefault(
|
|
self.current_cot_key(), set()
|
|
)
|
|
|
|
def claim_root_for_current_pass(self, root: ClassMeta) -> None:
|
|
"""Record that one root has been claimed for this recursive pass."""
|
|
|
|
self.story_cursor.claimed_locations_this_recursive_pass.add(root.location)
|
|
|
|
def current_cot_stabilized_files(self) -> list[ClassMeta]:
|
|
"""Return current metadata for roots stabilized during the active COT."""
|
|
|
|
files: list[ClassMeta] = []
|
|
for location in sorted(self.stabilized_locations_for_current_cot()):
|
|
current = self.file_manager.current_src(location)
|
|
if current is not None:
|
|
files.append(current)
|
|
continue
|
|
original = next(
|
|
(
|
|
candidate
|
|
for candidate in self.relevant_files
|
|
if candidate.location == location
|
|
),
|
|
None,
|
|
)
|
|
if original is not None:
|
|
files.append(original)
|
|
return files
|
|
|
|
def mark_root_visited(self, root: ClassMeta) -> None:
|
|
"""Record that one root was selected and make it the active stabilization story."""
|
|
|
|
self.visited_locations_for_current_cot().add(root.location)
|
|
self.claim_root_for_current_pass(root)
|
|
self.story_cursor.task_cursor.active_root = root
|
|
self.story_cursor.task_cursor.active_stabilization_key = (
|
|
f"{self.current_cot_key()}:{root.location}"
|
|
)
|
|
self.story_cursor.task_cursor.marginal_result_start = len(
|
|
self.story_cursor.marginal_result
|
|
)
|
|
self.persist()
|
|
|
|
def mark_root_stabilized(
|
|
self,
|
|
root: ClassMeta,
|
|
summary: str,
|
|
story_roots: list[ClassMeta] | None = None,
|
|
) -> None:
|
|
"""Record a completed root stabilization and clear active stabilization state."""
|
|
|
|
files = list(story_roots or [root])
|
|
for file in files:
|
|
self.stabilized_locations_for_current_cot().add(file.location)
|
|
self.story_cursor.claimed_locations_this_recursive_pass.add(file.location)
|
|
self.story_cursor.run_summaries.append(summary)
|
|
self.record_stabilization_result(files, summary)
|
|
self.story_cursor.task_cursor.deferred_remediation_by_cot.pop(
|
|
self.current_cot_key(),
|
|
None,
|
|
)
|
|
self.clear_active_root()
|
|
|
|
def clear_active_root(self) -> None:
|
|
"""Clear the active root and stabilization identity after completion."""
|
|
|
|
self.story_cursor.task_cursor.active_root = None
|
|
self.story_cursor.task_cursor.active_stabilization_key = None
|
|
self.persist()
|
|
|
|
def set_current_cot_task(self, index: int) -> None:
|
|
"""Set the active COT task index."""
|
|
|
|
self.story_cursor.task_cursor.current_cot_index = max(
|
|
0,
|
|
min(index, len(self.story_cursor.cot_task_records)),
|
|
)
|
|
self.persist()
|
|
|
|
def refresh_relevant_files_from_current_metadata(self) -> None:
|
|
"""Rebuild pass relevance from current in-memory source metadata only."""
|
|
|
|
self._refresh_relevant_files()
|
|
self.persist()
|
|
|
|
def sync(self) -> None:
|
|
"""Refresh file metadata, pass-level relevance, and intent."""
|
|
|
|
if self._file_manager is None:
|
|
self._file_manager = self._create_file_manager()
|
|
else:
|
|
self._file_manager.prompt = self.goal
|
|
self._file_manager.resync()
|
|
|
|
self._refresh_relevant_files()
|
|
self._refresh_intent()
|
|
self.persist()
|
|
|
|
def begin_recursive_pass(self) -> bool:
|
|
"""Prepare one recursive pass through sync and intent without COT breakdown."""
|
|
|
|
self.update_global_iteration()
|
|
self.story_cursor.cot_task_records = []
|
|
self.story_cursor.task_cursor.current_cot_index = 0
|
|
self.story_cursor.claimed_locations_this_recursive_pass.clear()
|
|
self.story_cursor.bootstrap_involved_locations.clear()
|
|
self.story_cursor.task_cursor.visited_locations_by_cot.clear()
|
|
self.story_cursor.task_cursor.stabilized_locations_by_cot.clear()
|
|
self.story_cursor.task_cursor.active_root = None
|
|
self.story_cursor.task_cursor.active_stabilization_key = None
|
|
self.story_cursor.task_cursor.deferred_remediation_by_cot.clear()
|
|
self.story_cursor._bootstrap_started = False
|
|
self.story_cursor._bootstrap_completed = False
|
|
# self.sync()
|
|
if not self.relevant_files and not (
|
|
self.intent and self.intent & Intent.FEATURE
|
|
):
|
|
self.persist()
|
|
return False
|
|
self.persist()
|
|
return True
|
|
|
|
def update_global_iteration(self) -> None:
|
|
"""Increment the global outer-loop iteration counter."""
|
|
|
|
self.story_cursor.outer_iteration += 1
|
|
self.persist()
|