Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel
are credited as development tools, separately from human contributors.

### Fixed
- **Usage dashboard no longer blanks on one oversized non-transcript file** — a
single `.jsonl` with more than 1 MiB of leading lines that carry no
`timestamp` (such as a large workflow `journal.jsonl`, whose records hold
`type`/`key`/`result`/`agentId` and never a timestamp) made the
earliest-timestamp probe throw `TimestampProbeLimitError`. That rejection was
unhandled, so it aborted the whole load and left the dashboard empty on every
refresh (`failed=1`, `bytes=0` in diagnostics). The probe now fails soft for
that one file and the rest load normally.
- **Opus 5 context window** — `claude-opus-5` reported a 200 000-token window
as if it were exact, so the status bar and dashboard showed a context
percentage five times too high. Claude Code writes the plain `claude-opus-5`
Expand Down
28 changes: 23 additions & 5 deletions src/claudeUsageFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,29 @@ export async function readEarliestTimestamp(
export async function sortUsageFilesByEarliestTimestamp(
entries: readonly UsageFileFingerprint[],
): Promise<{ files: string[]; bytesRead: number }> {
const stamped = await mapWithConcurrency(entries, 8, async (entry) => ({
file: entry.path,
discoveryIndex: entry.discoveryIndex,
...(await readEarliestTimestamp(entry.path)),
}));
const stamped = await mapWithConcurrency(entries, 8, async (entry) => {
try {
return {
file: entry.path,
discoveryIndex: entry.discoveryIndex,
...(await readEarliestTimestamp(entry.path)),
};
} catch {
// A single file we cannot probe must not abort the whole load. This
// happens when a .jsonl has more than 1 MiB of leading lines with no
// `timestamp` (readEarliestTimestamp throws TimestampProbeLimitError) —
// e.g. a large workflow `journal.jsonl`, whose records carry no
// timestamp — or on a transient read error. Fall back to a neutral stamp
// so the file sorts by discovery order, exactly as a small no-timestamp
// file already does, and the main reader still processes it.
return {
file: entry.path,
discoveryIndex: entry.discoveryIndex,
timestampMs: 0,
bytesRead: 0,
};
}
});
stamped.sort((a, b) =>
a.timestampMs - b.timestampMs || a.discoveryIndex - b.discoveryIndex,
);
Expand Down
36 changes: 36 additions & 0 deletions src/test/claudeUsageFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,39 @@ test('equal or invalid timestamps preserve v2.2.0 discovery order', async () =>
]);
assert.deepEqual(invalidPair.files, [invalid, invalid2]);
});

test('a single unprobeable file does not abort ordering the rest', async () => {
const { root } = await fixture();
const project = path.join(root, 'projects', '-tmp-demo');
const good = path.join(project, 'good.jsonl');
const journal = path.join(project, 'journal.jsonl');
await writeFile(good, '{"timestamp":"2026-01-01T00:00:00.000Z"}\n');
// A large workflow journal.jsonl: >1 MiB of records that never carry a
// `timestamp`, which makes readEarliestTimestamp throw TimestampProbeLimitError.
await writeFile(journal, '{"type":"result","key":"k","agentId":"a"}\n'.repeat(40_000));
await assert.rejects(
() => readEarliestTimestamp(journal),
{ name: 'TimestampProbeLimitError' },
);

const make = async (file: string, discoveryIndex: number) => {
const fileStat = await stat(file);
return {
path: file,
size: fileStat.size,
mtimeMs: fileStat.mtimeMs,
discoveryIndex,
dev: fileStat.dev > 0 && fileStat.ino > 0 ? fileStat.dev : undefined,
ino: fileStat.dev > 0 && fileStat.ino > 0 ? fileStat.ino : undefined,
};
};

// Before the fix this rejected — one unprobeable file zeroed the whole load.
// Now it resolves and keeps every file, the unprobeable one stamped 0 so it
// sorts ahead of the real timestamp by discovery order.
const sorted = await sortUsageFilesByEarliestTimestamp([
await make(journal, 0),
await make(good, 1),
]);
assert.deepEqual(sorted.files, [journal, good]);
});
Loading