Skip to content
Open
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
20 changes: 20 additions & 0 deletions packages/shared/src/usageMerge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,26 @@ describe("mergeUsage", () => {
).toEqual({ claude: 1, codex: 1 });
});

it("uses the newest scan when environments share the same transcript directory", () => {
const source = { provider: "claude" as const, hostId: "mac", homePath: "/home/theo/.claude" };
const environments = [
environment("env-a", summary([bucket({ costUsd: 4, records: 2 })], [source])),
environment("env-b", {
...summary([bucket()], [source]),
readAt: "2026-08-07T01:00:00.000Z",
}),
];

for (const ordered of [environments, environments.toReversed()]) {
const merged = mergeUsage(ordered, USAGE_CONTRACT_VERSION);
expect(merged.costUsd).toBe(10);
expect(merged.records).toBe(5);
expect(merged.sessions).toBe(1);
expect(merged.contributingEnvironments).toEqual(["env-b"]);
expect(merged.duplicateSources).toEqual(["env-a: /home/theo/.claude"]);
}
});

it("excludes an environment reporting an older contract version", () => {
const merged = mergeUsage(
[
Expand Down
12 changes: 8 additions & 4 deletions packages/shared/src/usageMerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ function fingerprintKey(fingerprint: UsageSourceFingerprint): string {
*
* Several environments on one machine (worktree servers, for instance) resolve
* the same provider home and would otherwise double count every token. The
* first environment in a stable order claims a fingerprint; the rest have that
* provider's buckets dropped. Environments are sorted by id so the winner does
* not change between renders.
* most recently read summary claims a fingerprint; the rest have that provider's
* buckets dropped. Environment ids break ties so the winner is stable when
* summaries have the same read time.
*/
function claimSources(environments: readonly EnvironmentUsage[]): {
readonly ownerByFingerprint: ReadonlyMap<string, EnvironmentId>;
Expand All @@ -116,7 +116,11 @@ function claimSources(environments: readonly EnvironmentUsage[]): {
const ownerByFingerprint = new Map<string, EnvironmentId>();
const duplicates: string[] = [];

const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId));
const ordered = [...environments].sort(
(a, b) =>
(Date.parse(b.summary.readAt) || 0) - (Date.parse(a.summary.readAt) || 0) ||
a.environmentId.localeCompare(b.environmentId),
);

for (const environment of ordered) {
for (const source of environment.summary.sources) {
Expand Down
Loading