fix: skip cache-ttl append after compaction to prevent double compaction (#28548)
Merged via squash. Prepared head SHA: a4114a52bcff6ed4057cc54d3c629bd723f3d420 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:
parent
7332e6d609
commit
9cd54ea882
@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated `session.store` roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.
|
||||
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
||||
- Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when `hooks.allowedAgentIds` leaves hook routing unrestricted.
|
||||
- Agents/compaction: skip the post-compaction `cache-ttl` marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
|
||||
@ -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" &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user