mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
fix: skip cache-ttl append after compaction to prevent double compaction (#28548)
Merged via squash.
Prepared head SHA: a4114a52bc
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -257,6 +257,14 @@ const testModel = {
|
||||
input: ["text"],
|
||||
} as unknown as Model<Api>;
|
||||
|
||||
const cacheTtlEligibleModel = {
|
||||
api: "anthropic",
|
||||
provider: "anthropic",
|
||||
compat: {},
|
||||
contextWindow: 8192,
|
||||
input: ["text"],
|
||||
} as unknown as Model<Api>;
|
||||
|
||||
describe("runEmbeddedAttempt sessions_spawn workspace inheritance", () => {
|
||||
const tempPaths: string[] = [];
|
||||
|
||||
@@ -382,6 +390,123 @@ describe("runEmbeddedAttempt sessions_spawn workspace inheritance", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("runEmbeddedAttempt cache-ttl tracking after compaction", () => {
|
||||
const tempPaths: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
hoisted.createAgentSessionMock.mockReset();
|
||||
hoisted.sessionManagerOpenMock.mockReset().mockReturnValue(hoisted.sessionManager);
|
||||
hoisted.resolveSandboxContextMock.mockReset();
|
||||
hoisted.acquireSessionWriteLockMock.mockReset().mockResolvedValue({
|
||||
release: async () => {},
|
||||
});
|
||||
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
|
||||
hoisted.sessionManager.branch.mockReset();
|
||||
hoisted.sessionManager.resetLeaf.mockReset();
|
||||
hoisted.sessionManager.buildSessionContext.mockReset().mockReturnValue({ messages: [] });
|
||||
hoisted.sessionManager.appendCustomEntry.mockReset();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (tempPaths.length > 0) {
|
||||
const target = tempPaths.pop();
|
||||
if (target) {
|
||||
await fs.rm(target, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function runAttemptWithCacheTtl(compactionCount: number) {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cache-ttl-workspace-"));
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cache-ttl-agent-"));
|
||||
const sessionFile = path.join(workspaceDir, "session.jsonl");
|
||||
tempPaths.push(workspaceDir, agentDir);
|
||||
await fs.writeFile(sessionFile, "", "utf8");
|
||||
|
||||
hoisted.subscribeEmbeddedPiSessionMock.mockReset().mockImplementation(() => ({
|
||||
...createSubscriptionMock(),
|
||||
getCompactionCount: () => compactionCount,
|
||||
}));
|
||||
|
||||
hoisted.createAgentSessionMock.mockImplementation(async () => {
|
||||
const session: MutableSession = {
|
||||
sessionId: "embedded-session",
|
||||
messages: [],
|
||||
isCompacting: false,
|
||||
isStreaming: false,
|
||||
agent: {
|
||||
replaceMessages: (messages: unknown[]) => {
|
||||
session.messages = [...messages];
|
||||
},
|
||||
},
|
||||
prompt: async () => {
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
{ role: "assistant", content: "done", timestamp: 2 },
|
||||
];
|
||||
},
|
||||
abort: async () => {},
|
||||
dispose: () => {},
|
||||
steer: async () => {},
|
||||
};
|
||||
|
||||
return { session };
|
||||
});
|
||||
|
||||
return await runEmbeddedAttempt({
|
||||
sessionId: "embedded-session",
|
||||
sessionKey: "agent:main:test-cache-ttl",
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
contextPruning: {
|
||||
mode: "cache-ttl",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: "hello",
|
||||
timeoutMs: 10_000,
|
||||
runId: `run-cache-ttl-${compactionCount}`,
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-20250514",
|
||||
model: cacheTtlEligibleModel,
|
||||
authStorage: {} as AuthStorage,
|
||||
modelRegistry: {} as ModelRegistry,
|
||||
thinkLevel: "off",
|
||||
senderIsOwner: true,
|
||||
disableMessageTool: true,
|
||||
});
|
||||
}
|
||||
|
||||
it("skips cache-ttl append when compaction completed during the attempt", async () => {
|
||||
const result = await runAttemptWithCacheTtl(1);
|
||||
|
||||
expect(result.promptError).toBeNull();
|
||||
expect(hoisted.sessionManager.appendCustomEntry).not.toHaveBeenCalledWith(
|
||||
"openclaw.cache-ttl",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("appends cache-ttl when no compaction completed during the attempt", async () => {
|
||||
const result = await runAttemptWithCacheTtl(0);
|
||||
|
||||
expect(result.promptError).toBeNull();
|
||||
expect(hoisted.sessionManager.appendCustomEntry).toHaveBeenCalledWith(
|
||||
"openclaw.cache-ttl",
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-20250514",
|
||||
timestamp: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
const tempPaths: string[] = [];
|
||||
const sessionKey = "agent:main:discord:channel:test-ctx-engine";
|
||||
|
||||
@@ -2542,14 +2542,19 @@ export async function runEmbeddedAttempt(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if ANY compaction occurred during the entire attempt (prompt + retry).
|
||||
// Using a cumulative count (> 0) instead of a delta check avoids missing
|
||||
// compactions that complete during activeSession.prompt() before the delta
|
||||
// baseline is sampled.
|
||||
const compactionOccurredThisAttempt = getCompactionCount() > 0;
|
||||
|
||||
// Append cache-TTL timestamp AFTER prompt + compaction retry completes.
|
||||
// Previously this was before the prompt, which caused a custom entry to be
|
||||
// inserted between compaction and the next prompt — breaking the
|
||||
// prepareCompaction() guard that checks the last entry type, leading to
|
||||
// double-compaction. See: https://github.com/openclaw/openclaw/issues/9282
|
||||
// Skip when timed out during compaction — session state may be inconsistent.
|
||||
// Also skip when compaction ran this attempt — appending a custom entry
|
||||
// after compaction would break the guard again. See: #28491
|
||||
if (!timedOutDuringCompaction && !compactionOccurredThisAttempt) {
|
||||
const shouldTrackCacheTtl =
|
||||
params.config?.agents?.defaults?.contextPruning?.mode === "cache-ttl" &&
|
||||
|
||||
Reference in New Issue
Block a user