fix: prefer deterministic session usage targets

This commit is contained in:
Tak Hoffman
2026-03-24 21:21:57 -05:00
parent df58b4f5fb
commit 7a7e4cd4c4
2 changed files with 59 additions and 3 deletions

View File

@@ -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,

View File

@@ -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<string, SessionEntry>,
): Map<string, { key: string; entry: SessionEntry }> {
const storeBySessionId = new Map<string, { key: string; entry: SessionEntry }>();
const matchesBySessionId = new Map<string, Array<[string, SessionEntry]>>();
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<string, { key: string; entry: SessionEntry }>();
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;