authorgravatar for 153842990+yashranaway@users.noreply.github.comAditya Garud <153842990+yashranaway@users.noreply.github.com> 2026-09-17 19:42:44+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 19:42:44+05:30
logd4d5d12e8ba086cfbf79ca3adeb4156b46ead665
tree93c52ee87ffd140b5e46e236079f6d6709358018
parent1ab2dfb5a7bd2996f79407b5d02cae6132a7626c
signature Signed by PGP key B5690EEEBB952194

fix(shared): keep the newest shared usage scan (#10315)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

2 files changed, 28 insertions(+), 4 deletions(-)

packages/shared/src/usageMerge.test.ts+20
...@@ -146,6 +146,26 @@ describe("mergeUsage", () => {...@@ -146,6 +146,26 @@ describe("mergeUsage", () => {
146 ).toEqual({ claude: 1, codex: 1 });146 ).toEqual({ claude: 1, codex: 1 });
147 });147 });
148148
149 it("uses the newest scan when environments share the same transcript directory", () => {
150 const source = { provider: "claude" as const, hostId: "mac", homePath: "/home/theo/.claude" };
151 const environments = [
152 environment("env-a", summary([bucket({ costUsd: 4, records: 2 })], [source])),
153 environment("env-b", {
154 ...summary([bucket()], [source]),
155 readAt: "2026-08-07T01:00:00.000Z",
156 }),
157 ];
158
159 for (const ordered of [environments, environments.toReversed()]) {
160 const merged = mergeUsage(ordered, USAGE_CONTRACT_VERSION);
161 expect(merged.costUsd).toBe(10);
162 expect(merged.records).toBe(5);
163 expect(merged.sessions).toBe(1);
164 expect(merged.contributingEnvironments).toEqual(["env-b"]);
165 expect(merged.duplicateSources).toEqual(["env-a: /home/theo/.claude"]);
166 }
167 });
168
149 it("excludes an environment reporting an older contract version", () => {169 it("excludes an environment reporting an older contract version", () => {
150 const merged = mergeUsage(170 const merged = mergeUsage(
151 [171 [
packages/shared/src/usageMerge.ts+8-4
...@@ -118,9 +118,9 @@ function fingerprintKey(fingerprint: UsageSourceFingerprint): string {...@@ -118,9 +118,9 @@ function fingerprintKey(fingerprint: UsageSourceFingerprint): string {
118 *118 *
119 * Several environments on one machine (worktree servers, for instance) resolve119 * Several environments on one machine (worktree servers, for instance) resolve
120 * the same provider home and would otherwise double count every token. The120 * the same provider home and would otherwise double count every token. The
121 * first environment in a stable order claims a fingerprint; the rest have that121 * most recently read summary claims a fingerprint; the rest have that provider's
122 * provider's buckets dropped. Environments are sorted by id so the winner does122 * buckets dropped. Environment ids break ties so the winner is stable when
123 * not change between renders.123 * summaries have the same read time.
124 */124 */
125function claimSources(environments: readonly EnvironmentUsage[]): {125function claimSources(environments: readonly EnvironmentUsage[]): {
126 readonly ownerByFingerprint: ReadonlyMap<string, EnvironmentId>;126 readonly ownerByFingerprint: ReadonlyMap<string, EnvironmentId>;
...@@ -129,7 +129,11 @@ function claimSources(environments: readonly EnvironmentUsage[]): {...@@ -129,7 +129,11 @@ function claimSources(environments: readonly EnvironmentUsage[]): {
129 const ownerByFingerprint = new Map<string, EnvironmentId>();129 const ownerByFingerprint = new Map<string, EnvironmentId>();
130 const duplicates: string[] = [];130 const duplicates: string[] = [];
131131
132 const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId));132 const ordered = [...environments].sort(
133 (a, b) =>
134 (Date.parse(b.summary.readAt) || 0) - (Date.parse(a.summary.readAt) || 0) ||
135 a.environmentId.localeCompare(b.environmentId),
136 );
133137
134 for (const environment of ordered) {138 for (const environment of ordered) {
135 for (const source of environment.summary.sources) {139 for (const source of environment.summary.sources) {