fix(core): preserve per-task tokenUsage across archival (#1908)

## The bug

Per-task token usage (and the cost figures derived from it) is
**silently dropped the moment a task is archived**. A task that burned
millions of tokens shows `0` — or nothing — everywhere once it leaves
the live board.

Root cause is a field whitelist plus a missing type field:

- `TaskStore.taskToArchiveEntry()` (`packages/core/src/store.ts`)
constructs the archived record from an **explicit property whitelist**.
It copies `modelId` / `modelProvider` / `planningModelId` / … but never
`task.tokenUsage`.
- `ArchivedTaskEntry` (`packages/core/src/types.ts`) has no `tokenUsage`
field, so even a stray copy would be dropped by the type.

At archival time the task is DB-hydrated and still carries `tokenUsage`,
and the live `tasks` row (with its `tokenUsage*` columns) is then
deleted — so the whitelist is the only place the data survives or dies.
Result: `archive.db` (`archived_tasks.taskJson`) never contains token
stats. Any tool that reports token/cost usage can only ever see the
small live working set, never the hundreds of finished tasks.

## The fix

Thread `tokenUsage` through the archive round-trip (5 lines, all
pass-through of the already-typed `TaskTokenUsage`):

- `ArchivedTaskEntry` gains an optional `tokenUsage?: TaskTokenUsage`
field.
- `taskToArchiveEntry()` copies `tokenUsage: task.tokenUsage` (write
path).
- `archiveEntryToTask()` and `unarchiveTask()` copy `tokenUsage:
entry.tokenUsage` (both restore paths), so restored tasks keep their
history too.

Because the archived entry is serialized into `taskJson`, no DB
migration/column is needed — the counts land in the existing JSON blob
and read back via `json_extract(taskJson, '$.tokenUsage.totalTokens')`
(and the `inputTokens` / `outputTokens` / `cachedTokens` /
`cacheWriteTokens` breakdown).

## Verification

Applied the equivalent change to the bundled `dist/bin.js` on a live
install and archived a 9.9M-token task into an isolated copy of the
store. The full breakdown survived into `archive.db`:

```
inputTokens=119  outputTokens=26100  cachedTokens=9637483  cacheWriteTokens=233674
totalTokens=9897376  modelId=claude-sonnet-4-6  (+ per-model split intact)
```

Without the change the same archival leaves `tokenUsage` absent from the
entry.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Archived tasks now keep token usage details when saved and restored,
so task history remains accurate across archive flows.
* Restored archived tasks now display the same usage accounting they had
before being archived.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-04 19:37:06 -07:00
committed by GitHub
2 changed files with 5 additions and 0 deletions

View File

@@ -2331,6 +2331,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
validatorModelId: entry.validatorModelId,
planningModelProvider: entry.planningModelProvider,
planningModelId: entry.planningModelId,
tokenUsage: entry.tokenUsage,
breakIntoSubtasks: entry.breakIntoSubtasks,
noCommitsExpected: entry.noCommitsExpected,
branchContext: entry.branchContext,
@@ -2470,6 +2471,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
validatorModelId: task.validatorModelId,
planningModelProvider: task.planningModelProvider,
planningModelId: task.planningModelId,
tokenUsage: task.tokenUsage,
breakIntoSubtasks: task.breakIntoSubtasks,
noCommitsExpected: task.noCommitsExpected,
baseBranch: task.baseBranch,
@@ -14919,6 +14921,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
validatorModelId: entry.validatorModelId,
planningModelProvider: entry.planningModelProvider,
planningModelId: entry.planningModelId,
tokenUsage: entry.tokenUsage,
breakIntoSubtasks: entry.breakIntoSubtasks,
noCommitsExpected: entry.noCommitsExpected,
modifiedFiles: entry.modifiedFiles,

View File

@@ -4955,6 +4955,8 @@ export interface ArchivedTaskEntry {
/** Optional: planning model override for triage agent */
planningModelProvider?: string;
planningModelId?: string;
/** Per-task token/cost accounting (input/output/cache) preserved across archival. */
tokenUsage?: TaskTokenUsage;
/** Optional: other metadata to preserve */
breakIntoSubtasks?: boolean;
noCommitsExpected?: boolean;