Files
openclaw/src/agents/compaction-usage.ts
openclaw-clownfish[bot] 9ba6ed1d5c fix(agents): preserve fresh usage after compaction (#93084)
* fix(compaction): preserve fresh usage after compaction

Co-authored-by: HollyChou <128659251+Hollychou924@users.noreply.github.com>

Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>

* fix(compaction): satisfy stale usage timestamp narrowing

* fix(clownfish): address review for ghcrawl-156678-autonomous-smoke (1)

Co-authored-by: HollyChou <128659251+Hollychou924@users.noreply.github.com>

Co-authored-by: de1ty <7804799+de1tydev@users.noreply.github.com>

Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>

Co-authored-by: Zhao Shiqi <109639815+425072024@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Zhao Shiqi <109639815+425072024@users.noreply.github.com>
2026-06-15 07:33:45 +08:00

81 lines
2.8 KiB
TypeScript

/**
* Shared helpers for clearing assistant usage snapshots invalidated by
* transcript compaction.
*/
import type { AgentMessage } from "./runtime/index.js";
import { makeZeroUsageSnapshot } from "./usage.js";
function parseCompactionUsageTimestamp(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Date.parse(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
export function stripStaleAssistantUsageBeforeLatestCompaction<TMessage extends AgentMessage>(
messages: TMessage[],
options: {
mutate?: boolean;
whenMissingCompactionSummary?: "preserve" | "zeroAssistantUsage";
} = {},
): TMessage[] {
let latestCompactionSummaryIndex = -1;
let latestCompactionTimestamp: number | null = null;
for (let i = 0; i < messages.length; i += 1) {
const entry = messages[i];
if (entry?.role !== "compactionSummary") {
continue;
}
latestCompactionSummaryIndex = i;
latestCompactionTimestamp = parseCompactionUsageTimestamp(
(entry as { timestamp?: unknown }).timestamp ?? null,
);
}
const hasCompactionSummary = latestCompactionSummaryIndex !== -1;
if (!hasCompactionSummary && options.whenMissingCompactionSummary !== "zeroAssistantUsage") {
return messages;
}
const out = options.mutate ? messages : [...messages];
let touched = false;
for (let i = 0; i < out.length; i += 1) {
const candidate = out[i] as
| (AgentMessage & { usage?: unknown; timestamp?: unknown })
| undefined;
if (!candidate || candidate.role !== "assistant") {
continue;
}
if (!candidate.usage || typeof candidate.usage !== "object") {
continue;
}
const messageTimestamp = parseCompactionUsageTimestamp(candidate.timestamp);
const compactionTimestamp = latestCompactionTimestamp;
const hasTimestampBoundary =
hasCompactionSummary && compactionTimestamp !== null && messageTimestamp !== null;
const staleByMissingSummary = !hasCompactionSummary;
const staleByTimestamp = hasTimestampBoundary && messageTimestamp <= compactionTimestamp;
const staleByLegacyOrdering =
hasCompactionSummary && !hasTimestampBoundary && i < latestCompactionSummaryIndex;
if (!staleByMissingSummary && !staleByTimestamp && !staleByLegacyOrdering) {
continue;
}
// Session runtime expects assistant usage to stay structurally valid during
// accounting. Keep stale snapshots present, but zeroed after compaction.
const candidateRecord = candidate as unknown as Record<string, unknown>;
out[i] = {
...candidateRecord,
usage: makeZeroUsageSnapshot(),
} as unknown as TMessage;
touched = true;
}
return touched ? out : messages;
}