diff --git a/packages/shared/src/usageMerge.test.ts b/packages/shared/src/usageMerge.test.ts index 24bbc3b7c2e8099aebec6ce461a55249ddf15c96..142d6922f0c4459c2f2b8f8f0c9b23a1fc3acb30 100644 --- a/packages/shared/src/usageMerge.test.ts +++ b/packages/shared/src/usageMerge.test.ts @@ -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( [ diff --git a/packages/shared/src/usageMerge.ts b/packages/shared/src/usageMerge.ts index e0cb0510eb71a4b64a0cb2ab2df72d6ef806455e..fee8e9c95666e803339f4aa6266b6705c29f4e56 100644 --- a/packages/shared/src/usageMerge.ts +++ b/packages/shared/src/usageMerge.ts @@ -118,9 +118,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; @@ -129,7 +129,11 @@ function claimSources(environments: readonly EnvironmentUsage[]): { const ownerByFingerprint = new Map(); 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) {