Merge claudedo/123b0241b5e94b69bfa592fde89c51aa

This commit is contained in:
mika kuns
2026-08-05 22:44:47 +02:00
9 changed files with 383 additions and 20 deletions
@@ -0,0 +1,53 @@
using ClaudeDo.Data.Models;
namespace ClaudeDo.Worker.Runner;
/// <summary>The task/list/preset/global resolution <see cref="TaskRunner"/> actually runs with,
/// plus where each value came from — so a caller can tell a genuine default from an override
/// without re-deriving the resolution order itself.</summary>
public sealed record EffectiveRunConfig(
string Model, string ModelSource,
int MaxTurns, string MaxTurnsSource, int RequestedMaxTurns, bool MaxTurnsClamped,
string Effort,
string? AgentPath, string? AgentPathSource,
string PermissionMode,
bool SystemPromptSet, IReadOnlyList<string> SystemPromptSources);
/// <summary>Single source of truth for "which value wins" — shared by <see cref="TaskRunner"/>'s
/// actual run path and any read-only reporting of the same resolution (e.g. the
/// <c>get_effective_run_config</c> MCP tool), so the two can never drift apart.</summary>
public static class EffectiveRunConfigResolver
{
public static EffectiveRunConfig Resolve(
TaskEntity task, ListConfigEntity? listConfig, AppSettingsEntity global,
string? systemFile, string? improvementPrompt)
{
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
var modelSource = task.Model is not null ? "task" : listConfig?.Model is not null ? "list" : "global";
var preset = ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns);
var requestedMaxTurns = task.MaxTurns ?? listConfig?.MaxTurns ?? preset.MaxTurns;
var maxTurnsSource = task.MaxTurns is not null ? "task" : listConfig?.MaxTurns is not null ? "list" : "preset";
// ResolveMaxTurns is declared int? but always returns a value (Math.Min of non-null inputs).
var maxTurns = TaskRunner.ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns, global.MaxTurnsCeiling)!.Value;
var agentPath = task.AgentPath ?? listConfig?.AgentPath;
var agentPathSource = task.AgentPath is not null ? "task" : listConfig?.AgentPath is not null ? "list" : null;
var systemPromptSources = new List<string>();
if (!string.IsNullOrWhiteSpace(systemFile)) systemPromptSources.Add("systemFile");
if (!string.IsNullOrWhiteSpace(improvementPrompt)) systemPromptSources.Add("improvementPrompt");
if (!string.IsNullOrWhiteSpace(global.DefaultClaudeInstructions)) systemPromptSources.Add("global");
if (!string.IsNullOrWhiteSpace(listConfig?.SystemPrompt)) systemPromptSources.Add("list");
if (!string.IsNullOrWhiteSpace(task.SystemPrompt)) systemPromptSources.Add("task");
return new EffectiveRunConfig(
model, modelSource,
maxTurns, maxTurnsSource, requestedMaxTurns, maxTurns < requestedMaxTurns,
preset.Effort,
agentPath, agentPathSource,
global.DefaultPermissionMode,
systemPromptSources.Count > 0, systemPromptSources);
}
}
+17 -15
View File
@@ -574,29 +574,25 @@ public sealed class TaskRunner
var requestedSkills = UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills);
var skillNames = await FilterToInstalledSkillsAsync(task.Id, requestedSkills, ct);
// The model decides the global effort/turn defaults: one preset row per model alias
// (Settings → General). List- and task-level max-turns overrides still win.
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
var preset = Data.Models.ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns);
var requestedMaxTurns = task.MaxTurns ?? listConfig?.MaxTurns ?? preset.MaxTurns;
var maxTurns = ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns, global.MaxTurnsCeiling);
if (maxTurns < requestedMaxTurns)
// Model/max-turns/effort/agent-path resolution is shared with get_effective_run_config
// so the two can never report different numbers for the same task.
var effective = EffectiveRunConfigResolver.Resolve(task, listConfig, global, systemFile, improvementPrompt);
if (effective.MaxTurnsClamped)
{
_logger.LogWarning(
"Task {TaskId}: max turns clamped to ceiling (requested={Requested}, effective={Effective}, ceiling={Ceiling})",
task.Id, requestedMaxTurns, maxTurns, global.MaxTurnsCeiling);
task.Id, effective.RequestedMaxTurns, effective.MaxTurns, global.MaxTurnsCeiling);
}
return new ClaudeRunConfig(
Model: model,
Model: effective.Model,
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
AgentPath: effective.AgentPath,
ResumeSessionId: resumeSessionId,
MaxTurns: maxTurns,
PermissionMode: global.DefaultPermissionMode,
MaxTurns: effective.MaxTurns,
PermissionMode: effective.PermissionMode,
SkillNames: skillNames,
Effort: preset.Effort);
Effort: effective.Effort);
}
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(
@@ -612,7 +608,7 @@ public sealed class TaskRunner
}
var installedNames = installed.Select(s => s.Name).ToHashSet(StringComparer.Ordinal);
var resolved = requestedSkills.Where(installedNames.Contains).ToList();
var resolved = FilterToInstalled(requestedSkills, installedNames);
var dropped = requestedSkills.Where(n => !installedNames.Contains(n)).ToList();
if (dropped.Count > 0)
{
@@ -624,6 +620,12 @@ public sealed class TaskRunner
return resolved;
}
/// <summary>Shared with get_effective_run_config so reported skill names match what a run
/// would actually filter down to.</summary>
internal static IReadOnlyList<string> FilterToInstalled(
IReadOnlyList<string> requestedSkills, IReadOnlySet<string> installedNames)
=> requestedSkills.Where(installedNames.Contains).ToList();
internal static IReadOnlyList<string> UnionSkillNames(params string?[] jsonArrays)
{
var names = new List<string>();