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:
mika kuns
2026-08-05 15:44:20 +02:00
parent 83ea429b8a
commit 7d3d6d7b54
28 changed files with 1318 additions and 20 deletions
@@ -117,4 +117,58 @@ public sealed class UsageGateAndRunModelTests : IDisposable
Assert.Null(reloaded.Model);
}
[Fact]
public async Task TaskRun_cache_token_fields_roundtrip_when_set()
{
var list = new ListEntity { Id = "l3", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t3", ListId = "l3", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r3",
TaskId = "t3",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
CacheReadTokens = 12345,
CacheWriteTokens = 678,
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r3");
Assert.Equal(12345, reloaded.CacheReadTokens);
Assert.Equal(678, reloaded.CacheWriteTokens);
}
[Fact]
public async Task TaskRun_cache_token_fields_stay_null_when_not_set()
{
var list = new ListEntity { Id = "l4", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t4", ListId = "l4", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r4",
TaskId = "t4",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r4");
Assert.Null(reloaded.CacheReadTokens);
Assert.Null(reloaded.CacheWriteTokens);
}
}