fix(worker): normalize model alias before preset lookup, wire DefaultMaxTurns as fallback
A full model id like claude-sonnet-4-6 in list_config.model never matched any ModelPresets row (only bare aliases did), so every run under it landed on the hardcoded 30-turn/DefaultLevel fallback instead of the sonnet preset row - this is what killed two autonomous tasks at the 30-turn limit. ModelRegistry.TryNormalizeAlias (non-throwing: exact match, then substring match against a full model id) lets ModelPresets.For resolve a full model id to its alias's preset row. For a model that still doesn't resolve, the hardcoded 30 is replaced by a caller-supplied fallbackMaxTurns, and TaskRunner now passes AppSettings.DefaultMaxTurns there - so that setting has a real effect instead of being dead, matching the direction already noted in docs/open.md.
This commit is contained in:
@@ -14,7 +14,8 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
- **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) and `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`)
|
||||
- **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)` always returns a usable row. Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||
- **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)
|
||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||
|
||||
|
||||
@@ -46,20 +46,26 @@ public static class ModelPresets
|
||||
}
|
||||
|
||||
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
|
||||
/// callers don't have to special-case an unconfigured or unknown model.</summary>
|
||||
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model)
|
||||
/// callers don't have to special-case an unconfigured or unknown model. <paramref name="model"/>
|
||||
/// is resolved through <see cref="ModelRegistry.TryNormalizeAlias"/> first, so a full CLI model
|
||||
/// id (e.g. "claude-sonnet-4-6") still hits its alias's preset row instead of falling through.
|
||||
/// If <paramref name="model"/> normalizes to nothing recognized, <paramref name="fallbackMaxTurns"/>
|
||||
/// (typically <c>AppSettings.DefaultMaxTurns</c>) is used instead of a hardcoded number, so a
|
||||
/// truly unknown model is no harsher than the configured global default.</summary>
|
||||
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model, int fallbackMaxTurns = 30)
|
||||
{
|
||||
var alias = (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||
var alias = ModelRegistry.TryNormalizeAlias(model) ?? (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||
foreach (var p in presets)
|
||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||
return p;
|
||||
foreach (var p in Defaults)
|
||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||
return p;
|
||||
return new ModelPreset(alias, EffortRegistry.DefaultLevel, 30);
|
||||
return new ModelPreset(alias, EffortRegistry.DefaultLevel, fallbackMaxTurns);
|
||||
}
|
||||
|
||||
public static ModelPreset For(string? json, string? model) => For(Parse(json), model);
|
||||
public static ModelPreset For(string? json, string? model, int fallbackMaxTurns = 30)
|
||||
=> For(Parse(json), model, fallbackMaxTurns);
|
||||
|
||||
// One row per known alias, canonical order, clamped values — so an edited or partial list
|
||||
// still round-trips into something every consumer can rely on.
|
||||
|
||||
@@ -26,4 +26,22 @@ public static class ModelRegistry
|
||||
return alias;
|
||||
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort alias lookup for run-path model resolution, where a full CLI model id
|
||||
/// (e.g. "claude-sonnet-4-6") is just as likely as a bare alias and a run must never be
|
||||
/// blocked by an unrecognized one. Null/blank/unrecognized → null; never throws.
|
||||
/// </summary>
|
||||
public static string? TryNormalizeAlias(string? model)
|
||||
{
|
||||
var m = model?.Trim();
|
||||
if (string.IsNullOrEmpty(m)) return null;
|
||||
foreach (var alias in Aliases)
|
||||
if (string.Equals(alias, m, StringComparison.OrdinalIgnoreCase))
|
||||
return alias;
|
||||
foreach (var alias in Aliases)
|
||||
if (m.Contains(alias, StringComparison.OrdinalIgnoreCase))
|
||||
return alias;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user