feat(roadblock): collect and strip CLAUDEDO_BLOCKED markers in StreamAnalyzer

This commit is contained in:
mika kuns
2026-06-04 14:15:45 +02:00
parent ac1e9b06de
commit cf7f0da400
2 changed files with 70 additions and 1 deletions

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Text.Json;
namespace ClaudeDo.Worker.Runner;
@@ -11,6 +12,7 @@ public sealed class StreamResult
public int TokensIn { get; set; }
public int TokensOut { get; set; }
public int ApiRetryCount { get; set; }
public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();
}
public sealed class StreamAnalyzer
@@ -22,6 +24,8 @@ public sealed class StreamAnalyzer
private int _tokensIn;
private int _tokensOut;
private int _apiRetryCount;
private readonly List<string> _blocks = new();
private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";
public void ProcessLine(string ndjsonLine)
{
@@ -39,7 +43,7 @@ public sealed class StreamAnalyzer
{
case "result":
if (root.TryGetProperty("result", out var resultProp))
_resultMarkdown = resultProp.GetString();
_resultMarkdown = StripAndCollect(resultProp.GetString());
if (root.TryGetProperty("structured_output", out var structuredProp))
_structuredOutputJson = structuredProp.ToString();
if (root.TryGetProperty("session_id", out var sessionProp))
@@ -56,6 +60,7 @@ public sealed class StreamAnalyzer
case "assistant":
_turnCount++;
CollectFromAssistant(root);
break;
case "system":
@@ -81,6 +86,7 @@ public sealed class StreamAnalyzer
TokensIn = _tokensIn,
TokensOut = _tokensOut,
ApiRetryCount = _apiRetryCount,
Blocks = _blocks,
};
private string? FallbackResult()
@@ -97,6 +103,37 @@ public sealed class StreamAnalyzer
return _structuredOutputJson;
}
private void CollectFromAssistant(JsonElement root)
{
if (!root.TryGetProperty("message", out var msg)) return;
if (msg.ValueKind != JsonValueKind.Object) return;
if (!msg.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) return;
foreach (var block in content.EnumerateArray())
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
&& block.TryGetProperty("text", out var txt))
ScanForBlocks(txt.GetString());
}
private void ScanForBlocks(string? text)
{
if (string.IsNullOrEmpty(text)) return;
foreach (var line in text.Split('\n'))
{
var trimmed = line.Trim();
if (trimmed.StartsWith(BlockedPrefix, StringComparison.Ordinal))
_blocks.Add(trimmed[BlockedPrefix.Length..].Trim());
}
}
private string? StripAndCollect(string? text)
{
if (string.IsNullOrEmpty(text)) return text;
ScanForBlocks(text);
var kept = text.Split('\n')
.Where(l => !l.Trim().StartsWith(BlockedPrefix, StringComparison.Ordinal));
return string.Join('\n', kept).Trim();
}
private void TryAccumulateUsage(JsonElement root)
{
if (!root.TryGetProperty("event", out var eventProp)) return;

View File

@@ -120,4 +120,36 @@ public sealed class StreamAnalyzerTests
Assert.Contains("output", result.ResultMarkdown);
Assert.Contains("42", result.ResultMarkdown);
}
[Fact]
public void Collects_Blocked_Markers_From_Assistant_Text()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}""");
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal(2, result.Blocks.Count);
Assert.Equal("missing API key", result.Blocks[0]);
Assert.Equal("cannot reach db", result.Blocks[1]);
}
[Fact]
public void Strips_Blocked_Markers_From_Result_Text()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown);
Assert.Single(result.Blocks);
Assert.Equal("no creds", result.Blocks[0]);
}
[Fact]
public void No_Markers_Means_Empty_Blocks()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
Assert.Empty(analyzer.GetResult().Blocks);
}
}