Skip to content

Commit 9ba6ed1

Browse files
openclaw-clownfish[bot]wyf027vincentkoc425072024
authored
fix(agents): preserve fresh usage after compaction (#93084)
* fix(compaction): preserve fresh usage after compaction Co-authored-by: HollyChou <[email protected]> Co-authored-by: 吴杨帆 <[email protected]> * fix(compaction): satisfy stale usage timestamp narrowing * fix(clownfish): address review for ghcrawl-156678-autonomous-smoke (1) Co-authored-by: HollyChou <[email protected]> Co-authored-by: de1ty <[email protected]> Co-authored-by: 吴杨帆 <[email protected]> Co-authored-by: Zhao Shiqi <[email protected]> --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Co-authored-by: 吴杨帆 <[email protected]> Co-authored-by: Vincent Koc <[email protected]> Co-authored-by: Zhao Shiqi <[email protected]>
1 parent f1b8827 commit 9ba6ed1

5 files changed

Lines changed: 304 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Docs: https://docs.openclaw.ai
2525

2626
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
2727
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
28-
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, and @TurboTheTurtle.
28+
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, preserve fresh post-compaction usage while clearing stale usage snapshots, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #50795, #50845, #82874, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, @Hollychou924, @leno23, and @TurboTheTurtle.
2929
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.
3030
- Memory, state, diagnostics, and config: split header-too-large embedding batches, keep QMD memory search enabled in transient mode, avoid SQLite WAL on NFS volumes, preserve recovery scheduling outside stuck-session warning backoff, and keep shell environment fallbacks contained in config write tests. (#92650, #92618, #92639, #91247, #92752) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, and @gnanam1990.
3131
- UI/mobile/TUI: preserve dashboard session parent lineage, WebChat backscroll, reset soft command args, sidebar session picker interactivity, collapsed workspace files, resolved `/model` confirmation refs, and stale foreground iOS Gateway reconnects. (#90658, #92622, #91353, #92705, #92779, #92773, #92552) Thanks @luoyanglang, @TurboTheTurtle, @zhouhe-xydt, @NianJiuZst, @shakkernerd, @NarahariRaghava, and @Solvely-Colin.

src/agents/compaction-usage.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Shared helpers for clearing assistant usage snapshots invalidated by
3+
* transcript compaction.
4+
*/
5+
import type { AgentMessage } from "./runtime/index.js";
6+
import { makeZeroUsageSnapshot } from "./usage.js";
7+
8+
function parseCompactionUsageTimestamp(value: unknown): number | null {
9+
if (typeof value === "number" && Number.isFinite(value)) {
10+
return value;
11+
}
12+
if (typeof value === "string") {
13+
const parsed = Date.parse(value);
14+
if (Number.isFinite(parsed)) {
15+
return parsed;
16+
}
17+
}
18+
return null;
19+
}
20+
21+
export function stripStaleAssistantUsageBeforeLatestCompaction<TMessage extends AgentMessage>(
22+
messages: TMessage[],
23+
options: {
24+
mutate?: boolean;
25+
whenMissingCompactionSummary?: "preserve" | "zeroAssistantUsage";
26+
} = {},
27+
): TMessage[] {
28+
let latestCompactionSummaryIndex = -1;
29+
let latestCompactionTimestamp: number | null = null;
30+
for (let i = 0; i < messages.length; i += 1) {
31+
const entry = messages[i];
32+
if (entry?.role !== "compactionSummary") {
33+
continue;
34+
}
35+
latestCompactionSummaryIndex = i;
36+
latestCompactionTimestamp = parseCompactionUsageTimestamp(
37+
(entry as { timestamp?: unknown }).timestamp ?? null,
38+
);
39+
}
40+
const hasCompactionSummary = latestCompactionSummaryIndex !== -1;
41+
if (!hasCompactionSummary && options.whenMissingCompactionSummary !== "zeroAssistantUsage") {
42+
return messages;
43+
}
44+
45+
const out = options.mutate ? messages : [...messages];
46+
let touched = false;
47+
for (let i = 0; i < out.length; i += 1) {
48+
const candidate = out[i] as
49+
| (AgentMessage & { usage?: unknown; timestamp?: unknown })
50+
| undefined;
51+
if (!candidate || candidate.role !== "assistant") {
52+
continue;
53+
}
54+
if (!candidate.usage || typeof candidate.usage !== "object") {
55+
continue;
56+
}
57+
58+
const messageTimestamp = parseCompactionUsageTimestamp(candidate.timestamp);
59+
const compactionTimestamp = latestCompactionTimestamp;
60+
const hasTimestampBoundary =
61+
hasCompactionSummary && compactionTimestamp !== null && messageTimestamp !== null;
62+
const staleByMissingSummary = !hasCompactionSummary;
63+
const staleByTimestamp = hasTimestampBoundary && messageTimestamp <= compactionTimestamp;
64+
const staleByLegacyOrdering =
65+
hasCompactionSummary && !hasTimestampBoundary && i < latestCompactionSummaryIndex;
66+
if (!staleByMissingSummary && !staleByTimestamp && !staleByLegacyOrdering) {
67+
continue;
68+
}
69+
70+
// Session runtime expects assistant usage to stay structurally valid during
71+
// accounting. Keep stale snapshots present, but zeroed after compaction.
72+
const candidateRecord = candidate as unknown as Record<string, unknown>;
73+
out[i] = {
74+
...candidateRecord,
75+
usage: makeZeroUsageSnapshot(),
76+
} as unknown as TMessage;
77+
touched = true;
78+
}
79+
return touched ? out : messages;
80+
}

src/agents/embedded-agent-runner/replay-history.ts

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
normalizeInputProvenance,
2020
} from "../../sessions/input-provenance.js";
2121
import { isTranscriptOnlyOpenClawAssistantMessage } from "../../shared/transcript-only-openclaw-assistant.js";
22+
import { stripStaleAssistantUsageBeforeLatestCompaction } from "../compaction-usage.js";
2223
import {
2324
downgradeOpenAIFunctionCallReasoningPairs,
2425
downgradeOpenAIReasoningBlocks,
@@ -172,71 +173,6 @@ function annotateInterSessionUserMessages(messages: AgentMessage[]): AgentMessag
172173
return touched ? out : messages;
173174
}
174175

175-
function parseMessageTimestamp(value: unknown): number | null {
176-
if (typeof value === "number" && Number.isFinite(value)) {
177-
return value;
178-
}
179-
if (typeof value === "string") {
180-
const parsed = Date.parse(value);
181-
if (Number.isFinite(parsed)) {
182-
return parsed;
183-
}
184-
}
185-
return null;
186-
}
187-
188-
function stripStaleAssistantUsageBeforeLatestCompaction(messages: AgentMessage[]): AgentMessage[] {
189-
let latestCompactionSummaryIndex = -1;
190-
let latestCompactionTimestamp: number | null = null;
191-
for (let i = 0; i < messages.length; i += 1) {
192-
const entry = messages[i];
193-
if (entry?.role !== "compactionSummary") {
194-
continue;
195-
}
196-
latestCompactionSummaryIndex = i;
197-
latestCompactionTimestamp = parseMessageTimestamp(
198-
(entry as { timestamp?: unknown }).timestamp ?? null,
199-
);
200-
}
201-
if (latestCompactionSummaryIndex === -1) {
202-
return messages;
203-
}
204-
205-
const out = [...messages];
206-
let touched = false;
207-
for (let i = 0; i < out.length; i += 1) {
208-
const candidate = out[i] as
209-
| (AgentMessage & { usage?: unknown; timestamp?: unknown })
210-
| undefined;
211-
if (!candidate || candidate.role !== "assistant") {
212-
continue;
213-
}
214-
if (!candidate.usage || typeof candidate.usage !== "object") {
215-
continue;
216-
}
217-
218-
const messageTimestamp = parseMessageTimestamp(candidate.timestamp);
219-
const staleByTimestamp =
220-
latestCompactionTimestamp !== null &&
221-
messageTimestamp !== null &&
222-
messageTimestamp <= latestCompactionTimestamp;
223-
const staleByLegacyOrdering = i < latestCompactionSummaryIndex;
224-
if (!staleByTimestamp && !staleByLegacyOrdering) {
225-
continue;
226-
}
227-
228-
// session runtime expects assistant usage to always be present during context
229-
// accounting. Keep stale snapshots structurally valid, but zeroed out.
230-
const candidateRecord = candidate as unknown as Record<string, unknown>;
231-
out[i] = {
232-
...candidateRecord,
233-
usage: makeZeroUsageSnapshot(),
234-
} as unknown as AgentMessage;
235-
touched = true;
236-
}
237-
return touched ? out : messages;
238-
}
239-
240176
function sanitizeUserReplayContent(message: AgentMessage): AgentMessage | null {
241177
if (!message || message.role !== "user") {
242178
return message;

0 commit comments

Comments
 (0)