diff --git a/src/gateway/server-methods/usage.sessions-usage.test.ts b/src/gateway/server-methods/usage.sessions-usage.test.ts index c5c57f4ecda..db25201b481 100644 --- a/src/gateway/server-methods/usage.sessions-usage.test.ts +++ b/src/gateway/server-methods/usage.sessions-usage.test.ts @@ -189,6 +189,46 @@ describe("sessions.usage", () => { } }); + it("prefers the deterministic store key when duplicate sessionIds exist", async () => { + const preferredKey = "agent:opus:acp:run-dup"; + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-usage-test-")); + + try { + await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => { + const agentSessionsDir = path.join(stateDir, "agents", "opus", "sessions"); + fs.mkdirSync(agentSessionsDir, { recursive: true }); + const sessionFile = path.join(agentSessionsDir, "run-dup.jsonl"); + fs.writeFileSync(sessionFile, "", "utf-8"); + + vi.mocked(loadCombinedSessionStoreForGateway).mockReturnValue({ + storePath: "(multiple)", + store: { + [preferredKey]: { + sessionId: "run-dup", + sessionFile: "run-dup.jsonl", + updatedAt: 1_000, + }, + "agent:other:main": { + sessionId: "run-dup", + sessionFile: "run-dup.jsonl", + updatedAt: 2_000, + }, + }, + }); + + const respond = await runSessionsUsage({ + ...BASE_USAGE_RANGE, + key: "agent:opus:run-dup", + }); + const sessions = expectSuccessfulSessionsUsage(respond); + expect(sessions).toHaveLength(1); + expect(sessions[0]?.key).toBe(preferredKey); + }); + } finally { + fs.rmSync(stateDir, { recursive: true, force: true }); + } + }); + it("rejects traversal-style keys in specific session usage lookups", async () => { const respond = await runSessionsUsage({ ...BASE_USAGE_RANGE, diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index 8298f34eb24..83e96083f65 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -21,6 +21,7 @@ import { type DiscoveredSession, } from "../../infra/session-cost-usage.js"; import { parseAgentSessionKey } from "../../routing/session-key.js"; +import { resolvePreferredSessionKeyForSessionIdMatches } from "../../sessions/session-id-resolution.js"; import { buildUsageAggregateTail, mergeUsageDailyLatency, @@ -252,10 +253,25 @@ type DiscoveredSessionWithAgent = DiscoveredSession & { agentId: string }; function buildStoreBySessionId( store: Record, ): Map { - const storeBySessionId = new Map(); + const matchesBySessionId = new Map>(); for (const [key, entry] of Object.entries(store)) { - if (entry?.sessionId) { - storeBySessionId.set(entry.sessionId, { key, entry }); + if (!entry?.sessionId) { + continue; + } + const matches = matchesBySessionId.get(entry.sessionId) ?? []; + matches.push([key, entry]); + matchesBySessionId.set(entry.sessionId, matches); + } + + const storeBySessionId = new Map(); + for (const [sessionId, matches] of matchesBySessionId) { + const preferredKey = resolvePreferredSessionKeyForSessionIdMatches(matches, sessionId); + if (!preferredKey) { + continue; + } + const preferredEntry = store[preferredKey]; + if (preferredEntry) { + storeBySessionId.set(sessionId, { key: preferredKey, entry: preferredEntry }); } } return storeBySessionId;