mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:04:58 +00:00
Agents: drop stale pre-compaction usage snapshots
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
|
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
|
||||||
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.
|
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.
|
||||||
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
|
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
|
||||||
|
- Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson.
|
||||||
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
|
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
|
||||||
- Plugins/Hooks: run legacy `before_agent_start` once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710.
|
- Plugins/Hooks: run legacy `before_agent_start` once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710.
|
||||||
- Models/Config: default missing Anthropic provider/model `api` fields to `anthropic-messages` during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123.
|
- Models/Config: default missing Anthropic provider/model `api` fields to `anthropic-messages` during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123.
|
||||||
|
|||||||
@@ -158,6 +158,102 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
expect(first.content as string).toContain("sourceSession=agent:main:req");
|
expect(first.content as string).toContain("sourceSession=agent:main:req");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("drops stale assistant usage snapshots kept before latest compaction summary", async () => {
|
||||||
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{ role: "user", content: "old context" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "old answer" }],
|
||||||
|
stopReason: "stop",
|
||||||
|
usage: {
|
||||||
|
input: 191_919,
|
||||||
|
output: 2_000,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 193_919,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "compactionSummary",
|
||||||
|
summary: "compressed",
|
||||||
|
tokensBefore: 191_919,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
] as unknown as AgentMessage[];
|
||||||
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
|
messages,
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
sessionManager: mockSessionManager,
|
||||||
|
sessionId: TEST_SESSION_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const staleAssistant = result.find((message) => message.role === "assistant") as
|
||||||
|
| (AgentMessage & { usage?: unknown })
|
||||||
|
| undefined;
|
||||||
|
expect(staleAssistant).toBeDefined();
|
||||||
|
expect(staleAssistant?.usage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves fresh assistant usage snapshots created after latest compaction summary", async () => {
|
||||||
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "pre-compaction answer" }],
|
||||||
|
stopReason: "stop",
|
||||||
|
usage: {
|
||||||
|
input: 120_000,
|
||||||
|
output: 3_000,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 123_000,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "compactionSummary",
|
||||||
|
summary: "compressed",
|
||||||
|
tokensBefore: 123_000,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{ role: "user", content: "new question" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "fresh answer" }],
|
||||||
|
stopReason: "stop",
|
||||||
|
usage: {
|
||||||
|
input: 1_000,
|
||||||
|
output: 250,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 1_250,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as unknown as AgentMessage[];
|
||||||
|
|
||||||
|
const result = await sanitizeSessionHistory({
|
||||||
|
messages,
|
||||||
|
modelApi: "openai-responses",
|
||||||
|
provider: "openai",
|
||||||
|
sessionManager: mockSessionManager,
|
||||||
|
sessionId: TEST_SESSION_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistants = result.filter((message) => message.role === "assistant") as Array<
|
||||||
|
AgentMessage & { usage?: unknown }
|
||||||
|
>;
|
||||||
|
expect(assistants).toHaveLength(2);
|
||||||
|
expect(assistants[0]?.usage).toBeUndefined();
|
||||||
|
expect(assistants[1]?.usage).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps reasoning-only assistant messages for openai-responses", async () => {
|
it("keeps reasoning-only assistant messages for openai-responses", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,35 @@ function annotateInterSessionUserMessages(messages: AgentMessage[]): AgentMessag
|
|||||||
return touched ? out : messages;
|
return touched ? out : messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripStaleAssistantUsageBeforeLatestCompaction(messages: AgentMessage[]): AgentMessage[] {
|
||||||
|
let latestCompactionSummaryIndex = -1;
|
||||||
|
for (let i = 0; i < messages.length; i += 1) {
|
||||||
|
if (messages[i]?.role === "compactionSummary") {
|
||||||
|
latestCompactionSummaryIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (latestCompactionSummaryIndex <= 0) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = [...messages];
|
||||||
|
let touched = false;
|
||||||
|
for (let i = 0; i < latestCompactionSummaryIndex; i += 1) {
|
||||||
|
const candidate = out[i] as (AgentMessage & { usage?: unknown }) | undefined;
|
||||||
|
if (!candidate || candidate.role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!candidate.usage || typeof candidate.usage !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const candidateRecord = candidate as unknown as Record<string, unknown>;
|
||||||
|
const { usage: _droppedUsage, ...rest } = candidateRecord;
|
||||||
|
out[i] = rest as unknown as AgentMessage;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
return touched ? out : messages;
|
||||||
|
}
|
||||||
|
|
||||||
function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
||||||
if (!schema || typeof schema !== "object") {
|
if (!schema || typeof schema !== "object") {
|
||||||
return [];
|
return [];
|
||||||
@@ -466,6 +495,8 @@ export async function sanitizeSessionHistory(params: {
|
|||||||
? sanitizeToolUseResultPairing(sanitizedToolCalls)
|
? sanitizeToolUseResultPairing(sanitizedToolCalls)
|
||||||
: sanitizedToolCalls;
|
: sanitizedToolCalls;
|
||||||
const sanitizedToolResults = stripToolResultDetails(repairedTools);
|
const sanitizedToolResults = stripToolResultDetails(repairedTools);
|
||||||
|
const sanitizedCompactionUsage =
|
||||||
|
stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults);
|
||||||
|
|
||||||
const isOpenAIResponsesApi =
|
const isOpenAIResponsesApi =
|
||||||
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
|
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
|
||||||
@@ -480,8 +511,8 @@ export async function sanitizeSessionHistory(params: {
|
|||||||
})
|
})
|
||||||
: false;
|
: false;
|
||||||
const sanitizedOpenAI = isOpenAIResponsesApi
|
const sanitizedOpenAI = isOpenAIResponsesApi
|
||||||
? downgradeOpenAIReasoningBlocks(sanitizedToolResults)
|
? downgradeOpenAIReasoningBlocks(sanitizedCompactionUsage)
|
||||||
: sanitizedToolResults;
|
: sanitizedCompactionUsage;
|
||||||
|
|
||||||
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
|
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
|
||||||
appendModelSnapshot(params.sessionManager, {
|
appendModelSnapshot(params.sessionManager, {
|
||||||
|
|||||||
Reference in New Issue
Block a user