{"record":{"id":"3d8d4719b372a259","repo":"shareAI-lab/learn-claude-code","slug":"max-turns-must-be-at-least-1","errorCode":null,"errorMessage":"max_turns must be at least 1","messagePattern":"max_turns must be at least 1","errorType":"validation","errorClass":"GoalError","httpStatus":null,"severity":"error","filePath":"s17_goal_loop/code.py","lineNumber":541,"sourceCode":"        },\n    },\n]\n\n\nclass AgentSession:\n    \"\"\"A small real agent loop with a goal Stop hook at the return boundary.\"\"\"\n\n    def __init__(\n        self,\n        client: Any,\n        model: str,\n        goal: GoalController,\n        workdir: Path,\n        max_turns: int | None = None,\n        background_running: Callable[[], bool] | None = None,\n    ):\n        if max_turns is not None and max_turns < 1:\n            raise GoalError(\"max_turns must be at least 1\")\n        self.client = client\n        self.model = model\n        self.goal = goal\n        self.workdir = workdir.resolve()\n        self.max_turns = max_turns\n        self.background_running = background_running or (lambda: False)\n        self.messages: list[dict[str, Any]] = []\n        self.total_tokens = 0\n        self.hooks: dict[str, list[Callable[..., Any]]] = {\n            \"UserPromptSubmit\": [],\n            \"PreToolUse\": [],\n            \"PostToolUse\": [],\n            \"Stop\": [],\n        }\n        self.register_hook(\"PreToolUse\", self._permission_hook)\n        self.register_hook(\"PreToolUse\", self._log_hook)\n        self.register_hook(\"PostToolUse\", self._large_output_hook)\n        self.register_hook(\"UserPromptSubmit\", self._context_hook)","sourceCodeStart":523,"sourceCodeEnd":559,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s17_goal_loop/code.py#L523-L559","documentation":"AgentSession was constructed with max_turns that is not None and is less than 1 (s17_goal_loop/code.py:541). max_turns bounds the agentic loop (each turn = one model call/tool round); zero or negative turns would forbid even one query, so the constructor rejects it.","triggerScenarios":"Instantiating AgentSession(..., max_turns=0) or max_turns=-2. Also passing a CLI/config value like --max-turns 0 that flows into the constructor unvalidated.","commonSituations":"Interpreting max_turns=0 as 'unlimited' (the library uses None for unlimited); arithmetic on user input (e.g. turns - 1) dipping below 1; config parsers yielding 0 for missing numeric fields.","solutions":["Use max_turns=None for unlimited, or an integer >= 1 for a bounded loop","Validate parsed config/CLI values before constructing the session","Guard arithmetic that derives max_turns from another number"],"exampleFix":"# before\nsession = AgentSession(client, model, goal, workdir, max_turns=0)\n\n# after\nsession = AgentSession(client, model, goal, workdir, max_turns=None)","handlingStrategy":"validation","validationCode":"max_turns = int(os.getenv(\"MAX_TURNS\", 0)) or None  # 0/absent -> unlimited\nif max_turns is not None and max_turns < 1:\n    raise SystemExit(\"MAX_TURNS must be >= 1 or unset for unlimited\")\nsession = AgentSession(client, model, goal, workdir, max_turns=max_turns)","typeGuard":"def is_valid_max_turns(value: object) -> bool:\n    return value is None or (isinstance(value, int) and not isinstance(value, bool) and value >= 1)","tryCatchPattern":null,"preventionTips":["Use None for unlimited turns; 0 is invalid, not 'off'","Validate parsed CLI/config numbers before constructing AgentSession","Watch arithmetic like max_turns - 1 that can dip below 1 at edges"],"tags":["goal-loop","config","validation","constructor"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}