Merge claudedo/ca6e55c0992b460e83ff186c076b4315
This commit is contained in:
@@ -105,6 +105,9 @@ public sealed class ClaudeProcess : IClaudeProcess
|
||||
TokensIn = streamResult.TokensIn,
|
||||
TokensOut = streamResult.TokensOut,
|
||||
Blocks = streamResult.Blocks,
|
||||
ResultSubtype = streamResult.ResultSubtype,
|
||||
TerminalReason = streamResult.TerminalReason,
|
||||
Errors = streamResult.Errors,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,12 +118,19 @@ public sealed class ClaudeProcess : IClaudeProcess
|
||||
return new RunResult
|
||||
{
|
||||
ExitCode = exitCode,
|
||||
// Kept even on failure: a terminal reason like api_error often carries the
|
||||
// provider's own message (e.g. session-limit + reset time) in this field,
|
||||
// with nothing useful on stderr.
|
||||
ResultMarkdown = streamResult.ResultMarkdown,
|
||||
ErrorMarkdown = error,
|
||||
SessionId = streamResult.SessionId,
|
||||
TurnCount = streamResult.TurnCount,
|
||||
TokensIn = streamResult.TokensIn,
|
||||
TokensOut = streamResult.TokensOut,
|
||||
Blocks = streamResult.Blocks,
|
||||
ResultSubtype = streamResult.ResultSubtype,
|
||||
TerminalReason = streamResult.TerminalReason,
|
||||
Errors = streamResult.Errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ClaudeDo.Worker.Runner;
|
||||
|
||||
public sealed class RunResult
|
||||
public sealed record RunResult
|
||||
{
|
||||
public required int ExitCode { get; init; }
|
||||
public string? ResultMarkdown { get; init; }
|
||||
@@ -11,6 +11,9 @@ public sealed class RunResult
|
||||
public int TokensIn { get; init; }
|
||||
public int TokensOut { get; init; }
|
||||
public IReadOnlyList<string> Blocks { get; init; } = Array.Empty<string>();
|
||||
public string? ResultSubtype { get; init; }
|
||||
public string? TerminalReason { get; init; }
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
|
||||
public bool IsSuccess => ExitCode == 0 && ResultMarkdown is not null;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ public sealed class StreamResult
|
||||
public int TokensOut { get; set; }
|
||||
public int ApiRetryCount { get; set; }
|
||||
public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();
|
||||
public string? ResultSubtype { get; set; }
|
||||
public string? TerminalReason { get; set; }
|
||||
public IReadOnlyList<string> Errors { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class StreamAnalyzer
|
||||
@@ -25,6 +28,9 @@ public sealed class StreamAnalyzer
|
||||
private int _tokensOut;
|
||||
private int _apiRetryCount;
|
||||
private readonly List<string> _blocks = new();
|
||||
private string? _resultSubtype;
|
||||
private string? _terminalReason;
|
||||
private readonly List<string> _errors = new();
|
||||
private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";
|
||||
|
||||
public void ProcessLine(string ndjsonLine)
|
||||
@@ -48,6 +54,17 @@ public sealed class StreamAnalyzer
|
||||
_structuredOutputJson = structuredProp.ToString();
|
||||
if (root.TryGetProperty("session_id", out var sessionProp))
|
||||
_sessionId = sessionProp.GetString();
|
||||
if (root.TryGetProperty("subtype", out var resultSubtypeProp))
|
||||
_resultSubtype = resultSubtypeProp.GetString();
|
||||
if (root.TryGetProperty("terminal_reason", out var terminalReasonProp))
|
||||
_terminalReason = terminalReasonProp.GetString();
|
||||
if (root.TryGetProperty("errors", out var errorsProp) && errorsProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var errorItem in errorsProp.EnumerateArray())
|
||||
if (errorItem.ValueKind == JsonValueKind.String && errorItem.GetString() is { } errorText
|
||||
&& !string.IsNullOrEmpty(errorText))
|
||||
_errors.Add(errorText);
|
||||
}
|
||||
// Authoritative token totals live on the result event.
|
||||
if (root.TryGetProperty("usage", out var resultUsage))
|
||||
{
|
||||
@@ -87,6 +104,9 @@ public sealed class StreamAnalyzer
|
||||
TokensOut = _tokensOut,
|
||||
ApiRetryCount = _apiRetryCount,
|
||||
Blocks = _blocks.Distinct().ToList(),
|
||||
ResultSubtype = _resultSubtype,
|
||||
TerminalReason = _terminalReason,
|
||||
Errors = _errors,
|
||||
};
|
||||
|
||||
private string? FallbackResult()
|
||||
|
||||
@@ -352,6 +352,9 @@ public sealed class TaskRunner
|
||||
},
|
||||
ct);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
result = result with { ErrorMarkdown = BuildFailureMarkdown(result, config.MaxTurns) };
|
||||
|
||||
// Update the run record with results. Use CancellationToken.None:
|
||||
// this is a terminal write that must always complete, even if the
|
||||
// caller's token is already cancelled.
|
||||
@@ -597,6 +600,28 @@ public sealed class TaskRunner
|
||||
return string.Join("\n\n", trimmed);
|
||||
}
|
||||
|
||||
public static string? BuildFailureMarkdown(RunResult result, int? configuredMaxTurns)
|
||||
{
|
||||
switch (result.TerminalReason)
|
||||
{
|
||||
case "max_turns":
|
||||
var ofConfigured = configuredMaxTurns is int m ? $" von {m}" : "";
|
||||
return $"Turn-Budget erschöpft ({result.TurnCount}{ofConfigured} Turns). "
|
||||
+ "Erhöhe `maxTurns` am Task (`set_task_config`) und stelle den Task erneut in die Queue "
|
||||
+ "– der Worktree bleibt erhalten.";
|
||||
|
||||
case "api_error":
|
||||
if (!string.IsNullOrWhiteSpace(result.ResultMarkdown)) return result.ResultMarkdown;
|
||||
if (result.Errors.Count > 0) return string.Join(" ", result.Errors);
|
||||
return result.ErrorMarkdown;
|
||||
|
||||
default:
|
||||
return string.IsNullOrWhiteSpace(result.TerminalReason)
|
||||
? result.ErrorMarkdown
|
||||
: $"{result.ErrorMarkdown} (terminal_reason: {result.TerminalReason})";
|
||||
}
|
||||
}
|
||||
|
||||
public static string BuildRetryPrompt(string? capturedError)
|
||||
{
|
||||
var basePrompt = PromptFiles.ReadOrDefault(PromptKind.Retry);
|
||||
|
||||
Reference in New Issue
Block a user