feat(worker): clamp max-turns to a configurable ceiling
Runaway sessions were the single biggest cost driver: model_presets was never persisted (stayed code-only), default_max_turns shipped at 100, and ResolveMaxTurns had no upper bound, so a task/list override could run hundreds of turns unchecked. - TaskRunner.ResolveMaxTurns now clamps the resolved value to AppSettings.MaxTurnsCeiling (new column, default 80) and logs a warning with task id / requested / effective value when it clamps. - default_max_turns default lowered from 100 to 40 (entity, EF config, and the seeded row via the new AddMaxTurnsCeiling migration). - AppSettingsRepository.GetAsync backfills model_presets with the shipping defaults on first read instead of leaving the column null. - Settings > General's per-model preset table and the task/list agent editor now show a hint when a set max-turns value exceeds the ceiling.
This commit is contained in:
@@ -13,7 +13,7 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100)
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets` — `AppSettingsRepository.GetAsync` backfills shipping defaults into this column on the first read after it's null, so it's never null once a run has started), `MaxTurnsCeiling` (int, default 80, column `max_turns_ceiling` — hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run starts; `AppSettingsRepository.UpdateAsync` clamps it to a minimum of 1), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100). `DefaultMaxTurns` default was lowered from 100 to 40 (entity default + `AddMaxTurnsCeiling` migration backfill on the seeded row).
|
||||
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and never throw — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns = 30)` always returns a usable row: `model` is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`) still hits its alias's preset row instead of missing every lookup and falling through; only a model that normalizes to nothing recognized falls back to a synthesized row (`EffortRegistry.DefaultLevel` + `fallbackMaxTurns` — callers pass `AppSettings.DefaultMaxTurns` here so that setting has a real effect instead of a hardcoded number). Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||
- **ModelRegistry.TryNormalizeAlias** — non-throwing counterpart to `NormalizeAlias` for the run path: exact alias match, then substring match against a full model id, else `null`. Never throws, unlike `NormalizeAlias` (which stays the strict, throwing validator for `add_task`/planning model input).
|
||||
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
|
||||
@@ -45,7 +45,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
||||
|
||||
## Schema
|
||||
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. Migration `AddMaxTurnsCeiling` added `app_settings.max_turns_ceiling` (default 80) and lowered `default_max_turns`'s default/seeded value from 100 to 40. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user