fix(worker): record real raw token usage per run, not the uncached remainder
task_runs.tokens_in only ever held the API's uncached "input_tokens" field (off by a factor of ~400,000 on a resumed session), and tokens_out summed only the last result event instead of the whole session. TaskRunner now reads each run's cache-read/cache-write/input/output totals from the session transcript via a new ITranscriptUsageReader.ReadSessionTotalsAsync, storing the delta against prior runs on the same session so a --resume doesn't double-count. New task_runs.cache_read_tokens/cache_write_tokens columns; the Session tab now shows the raw total (what actually counts against the 5h/7d usage limit) with a breakdown tooltip.
This commit is contained in:
@@ -199,4 +199,69 @@ public class TranscriptUsageReaderTests : IDisposable
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionTotalsAsync_Sums_All_Assistant_Messages_In_The_Session_File()
|
||||
{
|
||||
WriteSession("proj", "sess-1.jsonl",
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 100, 5, requestId: "r1"),
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "claude-sonnet-5", 3, 7, 200, 0, requestId: "r2"));
|
||||
|
||||
var reader = MakeReader();
|
||||
var totals = await reader.ReadSessionTotalsAsync("sess-1");
|
||||
|
||||
Assert.NotNull(totals);
|
||||
Assert.Equal(13, totals!.InputTokens);
|
||||
Assert.Equal(27, totals.OutputTokens);
|
||||
Assert.Equal(300, totals.CacheReadTokens);
|
||||
Assert.Equal(5, totals.CacheCreationTokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionTotalsAsync_Skips_Synthetic_Model_Messages()
|
||||
{
|
||||
WriteSession("proj", "sess-2.jsonl",
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "r1"),
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "<synthetic>", 999, 999, 999, 999, requestId: "r2"));
|
||||
|
||||
var reader = MakeReader();
|
||||
var totals = await reader.ReadSessionTotalsAsync("sess-2");
|
||||
|
||||
Assert.NotNull(totals);
|
||||
Assert.Equal(10, totals!.InputTokens);
|
||||
Assert.Equal(20, totals.OutputTokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionTotalsAsync_Dedupes_By_RequestId_Within_The_Session()
|
||||
{
|
||||
WriteSession("proj", "sess-3.jsonl",
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"),
|
||||
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:01Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"));
|
||||
|
||||
var reader = MakeReader();
|
||||
var totals = await reader.ReadSessionTotalsAsync("sess-3");
|
||||
|
||||
Assert.NotNull(totals);
|
||||
Assert.Equal(10, totals!.InputTokens);
|
||||
Assert.Equal(20, totals.OutputTokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionTotalsAsync_Returns_Null_When_No_Matching_Transcript_File()
|
||||
{
|
||||
var reader = MakeReader();
|
||||
var totals = await reader.ReadSessionTotalsAsync("does-not-exist");
|
||||
|
||||
Assert.Null(totals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionTotalsAsync_Returns_Null_When_ProjectsRoot_Missing()
|
||||
{
|
||||
var reader = new TranscriptUsageReader(_cfg, Path.Combine(_root, "does-not-exist"));
|
||||
var totals = await reader.ReadSessionTotalsAsync("sess-1");
|
||||
|
||||
Assert.Null(totals);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user