feat(FN-4389): complete Step 1 — extend token usage mapping

Fusion-Task-Id: FN-4389
Fusion-Task-Lineage: 366f9d57-c476-448f-96c0-6e753fc8b090
This commit is contained in:
Fusion
2026-05-13 21:31:43 -07:00
committed by gsxdsm
parent b9dcda05c5
commit 3bda8b6a61
3 changed files with 45 additions and 13 deletions

View File

@@ -24,7 +24,8 @@ describe("TaskStore", () => {
inputTokens: 120,
outputTokens: 45,
cachedTokens: 30,
totalTokens: 195,
cacheWriteTokens: 9,
totalTokens: 204,
firstUsedAt: "2026-04-23T10:00:00.000Z",
lastUsedAt: "2026-04-23T10:05:00.000Z",
};
@@ -47,7 +48,8 @@ describe("TaskStore", () => {
inputTokens: 210,
outputTokens: 80,
cachedTokens: 40,
totalTokens: 330,
cacheWriteTokens: 15,
totalTokens: 345,
firstUsedAt: "2026-04-23T12:00:00.000Z",
lastUsedAt: "2026-04-23T12:30:00.000Z",
};
@@ -70,7 +72,8 @@ describe("TaskStore", () => {
inputTokens: 300,
outputTokens: 120,
cachedTokens: 50,
totalTokens: 470,
cacheWriteTokens: 25,
totalTokens: 495,
firstUsedAt: "2026-04-23T13:00:00.000Z",
lastUsedAt: "2026-04-23T13:45:00.000Z",
};
@@ -87,6 +90,27 @@ describe("TaskStore", () => {
expect(reloaded.tokenUsage).toEqual(tokenUsage);
});
it("round-trips cacheWriteTokens specifically", async () => {
const tokenUsage = {
inputTokens: 1,
outputTokens: 2,
cachedTokens: 3,
cacheWriteTokens: 1234,
totalTokens: 1240,
firstUsedAt: "2026-04-23T15:00:00.000Z",
lastUsedAt: "2026-04-23T15:01:00.000Z",
};
const task = await harness.store().createTask({
description: "Cache write token round-trip",
tokenUsage,
});
const detail = await harness.store().getTask(task.id);
expect(detail.tokenUsage?.cacheWriteTokens).toBe(1234);
expect(detail.tokenUsage).toEqual(tokenUsage);
});
it("clears token usage via null update and keeps it absent after reload", async () => {
// Cross-instance persistence test — see counterpart above.
harness.store().close();
@@ -98,7 +122,8 @@ describe("TaskStore", () => {
inputTokens: 99,
outputTokens: 44,
cachedTokens: 11,
totalTokens: 154,
cacheWriteTokens: 3,
totalTokens: 157,
firstUsedAt: "2026-04-23T14:00:00.000Z",
lastUsedAt: "2026-04-23T14:01:00.000Z",
},

View File

@@ -86,6 +86,7 @@ interface TaskRow {
tokenUsageInputTokens: number | null;
tokenUsageOutputTokens: number | null;
tokenUsageCachedTokens: number | null;
tokenUsageCacheWriteTokens: number | null;
tokenUsageTotalTokens: number | null;
tokenUsageFirstUsedAt: string | null;
tokenUsageLastUsedAt: string | null;
@@ -1015,6 +1016,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
row.tokenUsageInputTokens === null
|| row.tokenUsageOutputTokens === null
|| row.tokenUsageCachedTokens === null
|| row.tokenUsageCacheWriteTokens === null
|| row.tokenUsageTotalTokens === null
|| row.tokenUsageFirstUsedAt === null
|| row.tokenUsageLastUsedAt === null
@@ -1026,6 +1028,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
inputTokens: row.tokenUsageInputTokens,
outputTokens: row.tokenUsageOutputTokens,
cachedTokens: row.tokenUsageCachedTokens,
cacheWriteTokens: row.tokenUsageCacheWriteTokens,
totalTokens: row.tokenUsageTotalTokens,
firstUsedAt: row.tokenUsageFirstUsedAt,
lastUsedAt: row.tokenUsageLastUsedAt,
@@ -1331,7 +1334,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments",
"attachments", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
@@ -1380,7 +1383,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "attachments", "steeringComments",
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
@@ -1455,6 +1458,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.tokenUsage?.inputTokens ?? null,
task.tokenUsage?.outputTokens ?? null,
task.tokenUsage?.cachedTokens ?? null,
task.tokenUsage?.cacheWriteTokens ?? null,
task.tokenUsage?.totalTokens ?? null,
task.tokenUsage?.firstUsedAt ?? null,
task.tokenUsage?.lastUsedAt ?? null,
@@ -1522,14 +1526,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt,
dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(...this.getTaskPersistValues(task));
this.db.bumpLastModified();
@@ -1547,14 +1551,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt,
dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
ON CONFLICT(id) DO UPDATE SET
lineageId = excluded.lineageId,
@@ -1597,6 +1601,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
tokenUsageInputTokens = excluded.tokenUsageInputTokens,
tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
tokenUsageCacheWriteTokens = excluded.tokenUsageCacheWriteTokens,
tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,

View File

@@ -1100,9 +1100,11 @@ export interface TaskTokenUsage {
inputTokens: number;
/** Cumulative completion/output tokens consumed by the task. */
outputTokens: number;
/** Cumulative cache-hit tokens reported by providers. */
/** Cumulative cache-read (cache hit) tokens reported by providers. */
cachedTokens: number;
/** Cumulative total tokens for the task (input + output + cached semantics per provider reporting). */
/** Cumulative cache-write tokens reported by providers. */
cacheWriteTokens: number;
/** Cumulative total tokens for the task (input + output + cache-read + cache-write). */
totalTokens: number;
/** ISO-8601 timestamp of the first recorded usage event for this task. */
firstUsedAt: string;
@@ -4165,7 +4167,7 @@ export interface AgentHeartbeatRun {
/** Session ID after execution */
sessionIdAfter?: string;
/** Token usage for this run */
usageJson?: { inputTokens: number; outputTokens: number; cachedTokens: number };
usageJson?: { inputTokens: number; outputTokens: number; cachedTokens: number; cacheWriteTokens: number };
/** Structured result from the run */
resultJson?: Record<string, unknown>;
/** Snapshot of context at run start (taskId, projectId, etc.).