mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
Co-authored-by: pcpilot-dev <pcpilot-dev@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
83a3975eca
commit
4e651a43f4
@@ -4352,7 +4352,7 @@ describe("createTelegramBot", () => {
|
||||
sendMessageSpy.mockClear();
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
replySpy.mockResolvedValue({
|
||||
text: "⚙️ Compaction skipped: already_compacted_recently • ctx 0%",
|
||||
text: "⚙️ Compaction skipped: already_compacted • ctx 0%",
|
||||
});
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
|
||||
@@ -43,6 +43,10 @@ describe("classifyCompactionReason", () => {
|
||||
expect(classifyCompactionReason("already under target")).toBe("below_threshold");
|
||||
});
|
||||
|
||||
it('classifies "already compacted" without implying recency', () => {
|
||||
expect(classifyCompactionReason("already compacted")).toBe("already_compacted");
|
||||
});
|
||||
|
||||
it("classifies deferred background maintenance as a skip-like reason", () => {
|
||||
expect(classifyCompactionReason("deferred to background context-engine maintenance")).toBe(
|
||||
"deferred_background",
|
||||
@@ -63,7 +67,7 @@ describe("classifyCompactionReason", () => {
|
||||
});
|
||||
|
||||
describe("isBenignCompactionSkipReason", () => {
|
||||
it.each(["already under target", "already compacted recently"])(
|
||||
it.each(["already under target", "already compacted"])(
|
||||
"keeps the established %s skip contract",
|
||||
(reason) => {
|
||||
expect(isBenignCompactionSkipReason(reason)).toBe(true);
|
||||
|
||||
@@ -40,7 +40,7 @@ export function classifyCompactionReason(reason?: string): string {
|
||||
return "below_threshold";
|
||||
}
|
||||
if (text.includes("already compacted") || text.includes("already_compacted")) {
|
||||
return "already_compacted_recently";
|
||||
return "already_compacted";
|
||||
}
|
||||
if (text.includes("deferred to background")) {
|
||||
return "deferred_background";
|
||||
@@ -79,7 +79,7 @@ export function classifyCompactionReason(reason?: string): string {
|
||||
/** Return whether a classified reason represents an intentional compaction no-op. */
|
||||
export function isBenignCompactionSkipReason(reason?: string): boolean {
|
||||
const classification = classifyCompactionReason(reason);
|
||||
return classification === "below_threshold" || classification === "already_compacted_recently";
|
||||
return classification === "below_threshold" || classification === "already_compacted";
|
||||
}
|
||||
|
||||
/** Return whether a compaction result is an intentional no-op rather than a failure. */
|
||||
|
||||
@@ -3890,20 +3890,21 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fire after_compaction when compaction fails", async () => {
|
||||
it("does not fire after_compaction when the session is already compacted", async () => {
|
||||
hookRunner.hasHooks.mockReturnValue(true);
|
||||
const sync = vi.fn(async () => {});
|
||||
getMemorySearchManagerMock.mockResolvedValue({ manager: { sync } });
|
||||
contextEngineCompactMock.mockResolvedValue({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "nothing to compact",
|
||||
reason: "already_compacted",
|
||||
result: undefined,
|
||||
});
|
||||
|
||||
const result = await compactEmbeddedAgentSession(wrappedCompactionArgs());
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe("already_compacted");
|
||||
expect(hookRunner.runBeforeCompaction).toHaveBeenCalledTimes(1);
|
||||
expect(hookRunner.runAfterCompaction).not.toHaveBeenCalled();
|
||||
expect(sync).not.toHaveBeenCalled();
|
||||
|
||||
@@ -2664,7 +2664,117 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(compactCall.sessionFile).toContain("large-session.jsonl");
|
||||
});
|
||||
|
||||
it("skips OpenClaw maintenance when model policy routes a SQLite session to native-compacting claude-cli", async () => {
|
||||
it("byte-guards a Codex runtime session through SQLite semantic compaction", async () => {
|
||||
const storePath = path.join(rootDir, "sqlite-codex-byte-guard.json");
|
||||
const sessionKey = "agent:main:main";
|
||||
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
||||
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
||||
await replaceSqliteTranscriptEvents(scope, [
|
||||
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
|
||||
]);
|
||||
expect(readTranscriptStatsSync(scope).sizeBytes).toBeGreaterThan(10);
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 10,
|
||||
totalTokensFresh: true,
|
||||
compactionCount: 0,
|
||||
agentRuntimeOverride: "codex",
|
||||
agentHarnessId: "openclaw",
|
||||
};
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
const replyOperation = createReplyOperation();
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: { maxActiveTranscriptBytes: "10b" },
|
||||
},
|
||||
},
|
||||
},
|
||||
followupRun: createTestFollowupRun({
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
sessionId: "session",
|
||||
sessionKey,
|
||||
}),
|
||||
defaultModel: "gpt-5.5",
|
||||
agentCfgContextTokens: 1_000_000,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
isHeartbeat: false,
|
||||
replyOperation,
|
||||
});
|
||||
|
||||
expect(entry?.compactionCount).toBe(1);
|
||||
expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting");
|
||||
expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({
|
||||
agentHarnessId: "openclaw",
|
||||
deferOwningContextEngineCompaction: false,
|
||||
preflightCompactionTrigger: "transcript_bytes",
|
||||
preflightRequired: true,
|
||||
sessionId: "session",
|
||||
sessionKey,
|
||||
trigger: "budget",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves an under-limit SQLite Codex session to native token compaction", async () => {
|
||||
const storePath = path.join(rootDir, "sqlite-codex-under-byte-guard.json");
|
||||
const sessionKey = "agent:main:main";
|
||||
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
||||
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
||||
await replaceSqliteTranscriptEvents(scope, [
|
||||
{ message: { role: "user", content: "small" }, type: "message" },
|
||||
]);
|
||||
expect(readTranscriptStatsSync(scope).sizeBytes).toBeLessThan(10 * 1024);
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 347_000,
|
||||
totalTokensFresh: true,
|
||||
compactionCount: 0,
|
||||
agentRuntimeOverride: "codex",
|
||||
agentHarnessId: "openclaw",
|
||||
};
|
||||
const replyOperation = createReplyOperation();
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: { maxActiveTranscriptBytes: "10kb" },
|
||||
},
|
||||
},
|
||||
},
|
||||
followupRun: createTestFollowupRun({
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
sessionId: "session",
|
||||
sessionKey,
|
||||
}),
|
||||
defaultModel: "gpt-5.5",
|
||||
agentCfgContextTokens: 350_000,
|
||||
sessionEntry,
|
||||
sessionStore: { [sessionKey]: sessionEntry },
|
||||
sessionKey,
|
||||
storePath,
|
||||
isHeartbeat: false,
|
||||
replyOperation,
|
||||
});
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(replyOperation.setPhase).not.toHaveBeenCalled();
|
||||
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
||||
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps ownsNativeCompaction absolute over the SQLite transcript byte guard", async () => {
|
||||
registerClaudeCliBackend(true);
|
||||
registerMemoryFlushPlanResolverForTest(() => ({
|
||||
softThresholdTokens: 4_000,
|
||||
|
||||
@@ -796,15 +796,7 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
if (params.isHeartbeat || isCli || ownsNativeCompaction) {
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
if (normalizeLowercaseStringOrEmpty(runtimeId) === "codex") {
|
||||
// Codex runtime sessions should reach Codex with their real thread state.
|
||||
// Its harness owns automatic compaction; OpenClaw preflight compaction is
|
||||
// only for non-Codex embedded runtimes.
|
||||
logVerbose(
|
||||
`preflightCompaction skipped: sessionKey=${params.sessionKey} runtime=codex reason=codex_native_auto_compaction`,
|
||||
);
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
const isCodexRuntime = normalizeLowercaseStringOrEmpty(runtimeId) === "codex";
|
||||
|
||||
const compactionSessionKey = params.sessionKey ?? params.followupRun.run.sessionKey;
|
||||
if (!compactionSessionKey) {
|
||||
@@ -866,7 +858,7 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
const maxActiveTranscriptBytes = resolveMaxActiveTranscriptBytes(params.cfg);
|
||||
const shouldCheckActiveTranscriptBytes = typeof maxActiveTranscriptBytes === "number";
|
||||
const transcriptUsageTokens =
|
||||
typeof freshPersistedTokens === "number" && !freshNeedsOutputRead
|
||||
isCodexRuntime || (typeof freshPersistedTokens === "number" && !freshNeedsOutputRead)
|
||||
? undefined
|
||||
: await estimatePromptTokensFromSessionTranscript({
|
||||
agentId: compactionAgentId,
|
||||
@@ -893,6 +885,17 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
typeof activeTranscriptBytes === "number" &&
|
||||
typeof maxActiveTranscriptBytes === "number" &&
|
||||
activeTranscriptBytes >= maxActiveTranscriptBytes;
|
||||
if (isCodexRuntime && !shouldCompactByTranscriptBytes) {
|
||||
// Codex owns native-thread token pressure; OpenClaw owns the host transcript byte fuse
|
||||
// that bounds fresh-thread bootstrap seeds.
|
||||
logVerbose(
|
||||
`preflightCompaction skipped: sessionKey=${params.sessionKey} runtime=codex ` +
|
||||
`reason=codex_native_auto_compaction ` +
|
||||
`activeTranscriptBytes=${activeTranscriptBytes ?? "undefined"} ` +
|
||||
`maxActiveTranscriptBytes=${maxActiveTranscriptBytes ?? "undefined"}`,
|
||||
);
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
const stalePersistedPromptTokens =
|
||||
hasPersistedTotalTokens && entry.totalTokensFresh !== false
|
||||
? Math.floor(persistedTotalTokens)
|
||||
|
||||
@@ -343,11 +343,11 @@ describe("handleCompactCommand", () => {
|
||||
expect(vi.mocked(incrementCompactionCount)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats already_compacted_recently manual compaction as skipped", async () => {
|
||||
it("treats already_compacted manual compaction as skipped", async () => {
|
||||
vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "already_compacted_recently",
|
||||
reason: "already_compacted",
|
||||
});
|
||||
|
||||
const result = await handleCompactCommand(
|
||||
@@ -365,7 +365,7 @@ describe("handleCompactCommand", () => {
|
||||
);
|
||||
|
||||
expect(result?.reply?.text).toBe(
|
||||
"⚙️ Compaction skipped: session was already compacted recently • Context 12.1k",
|
||||
"⚙️ Compaction skipped: session is already compacted • Context 12.1k",
|
||||
);
|
||||
expect(result?.reply?.isStatusNotice).toBe(true);
|
||||
});
|
||||
|
||||
@@ -73,8 +73,8 @@ function formatCompactionReason(reason?: string): string | undefined {
|
||||
return lower.includes("already under target")
|
||||
? "context is already under the compaction target"
|
||||
: "context is below the compaction threshold";
|
||||
case "already_compacted_recently":
|
||||
return "session was already compacted recently";
|
||||
case "already_compacted":
|
||||
return "session is already compacted";
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user