fix: include reset/deleted transcripts in usage summaries

This commit is contained in:
robo7 2026-03-13 22:33:39 +08:00
parent 80e7da92ce
commit 524b638150
2 changed files with 100 additions and 4 deletions

View File

@ -231,6 +231,92 @@ describe("session cost usage", () => {
});
});
it("includes reset and deleted transcript files in aggregate usage summaries", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-archived-"));
const sessionsDir = path.join(root, "agents", "main", "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
const day = new Date("2026-03-13T10:00:00.000Z");
const files = [
"sess-active.jsonl",
"sess-reset.jsonl.reset.2026-03-13T10-05-00.000Z",
"sess-deleted.jsonl.deleted.2026-03-13T10-10-00.000Z",
];
for (const [i, name] of files.entries()) {
const filePath = path.join(sessionsDir, name);
await fs.writeFile(
filePath,
JSON.stringify({
type: "message",
timestamp: day.toISOString(),
message: {
role: "assistant",
provider: "openai",
model: "gpt-5.2",
usage: {
input: 10 + i,
output: 20 + i,
totalTokens: 30 + i * 2,
cost: { total: 0.01 * (i + 1) },
},
},
}),
"utf-8",
);
}
await withStateDir(root, async () => {
const summary = await loadCostUsageSummary({
startMs: new Date("2026-03-13T00:00:00.000Z").getTime(),
endMs: new Date("2026-03-13T23:59:59.999Z").getTime(),
});
expect(summary.daily).toHaveLength(1);
expect(summary.totals.totalTokens).toBe(96);
expect(summary.totals.totalCost).toBeCloseTo(0.06, 8);
});
});
it("discovers archived reset/deleted transcripts and preserves base session ids", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-discover-archived-"));
const sessionsDir = path.join(root, "agents", "main", "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
const files = [
["sess-active.jsonl", "active hello"],
["sess-reset.jsonl.reset.2026-03-13T10-05-00.000Z", "reset hello"],
["sess-deleted.jsonl.deleted.2026-03-13T10-10-00.000Z", "deleted hello"],
] as const;
for (const [name, text] of files) {
const filePath = path.join(sessionsDir, name);
await fs.writeFile(
filePath,
JSON.stringify({
type: "message",
timestamp: "2026-03-13T10:00:00.000Z",
message: {
role: "user",
content: text,
},
}),
"utf-8",
);
}
await withStateDir(root, async () => {
const sessions = await discoverAllSessions({
startMs: new Date("2026-03-13T00:00:00.000Z").getTime(),
});
const ids = sessions.map((session) => session.sessionId).sort();
expect(ids).toEqual(["sess-active", "sess-deleted", "sess-reset"]);
const reset = sessions.find((session) => session.sessionId === "sess-reset");
const deleted = sessions.find((session) => session.sessionId === "sess-deleted");
expect(reset?.firstUserMessage).toContain("reset hello");
expect(deleted?.firstUserMessage).toContain("deleted hello");
});
});
it("resolves non-main absolute sessionFile using explicit agentId for cost summary", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-agent-"));
const workerSessionsDir = path.join(root, "agents", "worker1", "sessions");

View File

@ -53,6 +53,16 @@ export type {
SessionUsageTimeSeries,
} from "./session-cost-usage.types.js";
const SESSION_TRANSCRIPT_FILE_RE = /\.jsonl(?:\.(?:reset|deleted)\..+)?$/;
const isSessionTranscriptFilename = (name: string): boolean =>
SESSION_TRANSCRIPT_FILE_RE.test(name);
const deriveSessionIdFromFilename = (name: string): string => {
const match = name.match(/^(.*?\.jsonl)(?:\.(?:reset|deleted)\..+)?$/);
return match ? match[1].slice(0, -6) : name.replace(/\.jsonl$/, "");
};
const emptyTotals = (): CostUsageTotals => ({
input: 0,
output: 0,
@ -318,7 +328,7 @@ export async function loadCostUsageSummary(params?: {
const files = (
await Promise.all(
entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
.filter((entry) => entry.isFile() && isSessionTranscriptFilename(entry.name))
.map(async (entry) => {
const filePath = path.join(sessionsDir, entry.name);
const stats = await fs.promises.stat(filePath).catch(() => null);
@ -393,7 +403,7 @@ export async function discoverAllSessions(params?: {
const discovered: DiscoveredSession[] = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
if (!entry.isFile() || !isSessionTranscriptFilename(entry.name)) {
continue;
}
@ -409,8 +419,8 @@ export async function discoverAllSessions(params?: {
}
// Do not exclude by endMs: a session can have activity in range even if it continued later.
// Extract session ID from filename (remove .jsonl)
const sessionId = entry.name.slice(0, -6);
// Extract session ID from filename, preserving base session id for archived reset/deleted transcripts.
const sessionId = deriveSessionIdFromFilename(entry.name);
// Try to read first user message for label extraction
let firstUserMessage: string | undefined;