- GetTaskLog reads at most last 256 KB; prepends truncation marker if file exceeds cap - Wrap temp-file cleanup in finally block to prevent leak on assertion failure - Add GetRun_NotFound_Throws, GetTaskLog_RunExistsButNoLogPath_Throws, and GetTaskLog_LargeFile_ReturnsTruncatedTail tests Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
2.8 KiB
C#
64 lines
2.8 KiB
C#
using System.ComponentModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ModelContextProtocol.Server;
|
|
|
|
namespace ClaudeDo.Worker.External;
|
|
|
|
public sealed record RunDto(
|
|
string Id, int RunNumber, string? SessionId, bool IsRetry,
|
|
string? ResultMarkdown, string? StructuredOutputJson, string? ErrorMarkdown,
|
|
int? ExitCode, int? TurnCount, int? TokensIn, int? TokensOut,
|
|
DateTime? StartedAt, DateTime? FinishedAt);
|
|
|
|
[McpServerToolType]
|
|
public sealed class RunHistoryMcpTools
|
|
{
|
|
private readonly TaskRunRepository _runs;
|
|
|
|
public RunHistoryMcpTools(TaskRunRepository runs) => _runs = runs;
|
|
|
|
[McpServerTool, Description("List all execution runs for a task (newest run metadata, tokens, turns, result, error).")]
|
|
public async Task<IReadOnlyList<RunDto>> ListRuns(string taskId, CancellationToken cancellationToken)
|
|
{
|
|
var runs = await _runs.GetByTaskIdAsync(taskId, cancellationToken);
|
|
return runs.Select(ToDto).ToList();
|
|
}
|
|
|
|
[McpServerTool, Description("Get a single execution run by its run id.")]
|
|
public async Task<RunDto> GetRun(string runId, CancellationToken cancellationToken)
|
|
{
|
|
var run = await _runs.GetByIdAsync(runId, cancellationToken)
|
|
?? throw new InvalidOperationException($"Run {runId} not found.");
|
|
return ToDto(run);
|
|
}
|
|
|
|
private const int MaxLogBytes = 256 * 1024;
|
|
|
|
[McpServerTool, Description("Fetch the raw log output of a task's latest run. Throws if no log is available.")]
|
|
public async Task<string> GetTaskLog(string taskId, CancellationToken cancellationToken)
|
|
{
|
|
var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken)
|
|
?? throw new InvalidOperationException($"No runs found for task {taskId}.");
|
|
if (string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath))
|
|
throw new InvalidOperationException("No log available for the latest run.");
|
|
|
|
var totalBytes = new FileInfo(run.LogPath).Length;
|
|
if (totalBytes <= MaxLogBytes)
|
|
return await File.ReadAllTextAsync(run.LogPath, cancellationToken);
|
|
|
|
var buffer = new byte[MaxLogBytes];
|
|
await using var fs = new FileStream(run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
fs.Seek(totalBytes - MaxLogBytes, SeekOrigin.Begin);
|
|
var read = await fs.ReadAsync(buffer, cancellationToken);
|
|
var tail = System.Text.Encoding.UTF8.GetString(buffer, 0, read);
|
|
return $"[truncated: showing last {MaxLogBytes} of {totalBytes} bytes]\n{tail}";
|
|
}
|
|
|
|
private static RunDto ToDto(TaskRunEntity r) => new(
|
|
r.Id, r.RunNumber, r.SessionId, r.IsRetry,
|
|
r.ResultMarkdown, r.StructuredOutputJson, r.ErrorMarkdown,
|
|
r.ExitCode, r.TurnCount, r.TokensIn, r.TokensOut,
|
|
r.StartedAt, r.FinishedAt);
|
|
}
|