mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-22 06:32:00 +00:00
fix(compaction): surface safeguard cancel reasons and clarify /compact skips (#51072)
Merged via squash.
Prepared head SHA: f1dbef0443
Co-authored-by: afurm <6375192+afurm@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
- MiniMax: add image generation provider for `image-01` model, supporting generate and image-to-image editing with aspect ratio control. (#54487) Thanks @liyuan97.
|
||||
- MiniMax: trim model catalog to M2.7 only, removing legacy M2, M2.1, M2.5, and VL-01 models. (#54487) Thanks @liyuan97.
|
||||
- Plugins/runtime: expose `runHeartbeatOnce` in the plugin runtime `system` namespace so plugins can trigger a single heartbeat cycle with an explicit delivery target override (e.g. `heartbeat: { target: "last" }`). (#40299) Thanks @loveyana.
|
||||
- Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -494,6 +495,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/routing: fail loud when `message send` targets an unknown non-default Telegram `accountId`, instead of silently falling back to the channel-level bot token and sending through the wrong bot. (#50853) Thanks @hclsys.
|
||||
- Web search: align onboarding, configure, and finalize with plugin-owned provider contracts, including disabled-provider recovery, config-aware credential hooks, and runtime-visible summaries. (#50935) Thanks @gumadeiras.
|
||||
- Agents/replay: sanitize malformed assistant tool-call replay blocks before provider replay so follow-up Anthropic requests do not inherit the downstream `replace` crash. (#50005) Thanks @jalehman.
|
||||
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
|
||||
- CLI/update status: explicitly say `up to date` when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.
|
||||
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
|
||||
- Discord/startup logging: report client initialization while the gateway is still connecting instead of claiming Discord is logged in before readiness is reached. (#51425) Thanks @scoootscoob.
|
||||
- Agents/compaction safeguard: preserve split-turn context and preserved recent turns when capped retry fallback reuses the last successful summary. (#27727) thanks @Pandadadadazxf.
|
||||
- Agents/memory flush: keep transcript-hash dedup active across memory-flush fallback retries so a write-then-throw flush attempt cannot append duplicate `MEMORY.md` entries before the fallback cycle completes. (#34222) Thanks @lml2468.
|
||||
|
||||
40
src/agents/pi-embedded-runner/compact-reasons.test.ts
Normal file
40
src/agents/pi-embedded-runner/compact-reasons.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifyCompactionReason, resolveCompactionFailureReason } from "./compact-reasons.js";
|
||||
|
||||
describe("resolveCompactionFailureReason", () => {
|
||||
it("replaces generic compaction cancellation with the safeguard reason", () => {
|
||||
expect(
|
||||
resolveCompactionFailureReason({
|
||||
reason: "Compaction cancelled",
|
||||
safeguardCancelReason:
|
||||
"Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6.",
|
||||
}),
|
||||
).toBe("Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6.");
|
||||
});
|
||||
|
||||
it("preserves non-generic compaction failures", () => {
|
||||
expect(
|
||||
resolveCompactionFailureReason({
|
||||
reason: "Compaction timed out",
|
||||
safeguardCancelReason:
|
||||
"Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6.",
|
||||
}),
|
||||
).toBe("Compaction timed out");
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyCompactionReason", () => {
|
||||
it('classifies "nothing to compact" as a skip-like reason', () => {
|
||||
expect(classifyCompactionReason("Nothing to compact (session too small)")).toBe(
|
||||
"no_compactable_entries",
|
||||
);
|
||||
});
|
||||
|
||||
it("classifies safeguard messages as guard-blocked", () => {
|
||||
expect(
|
||||
classifyCompactionReason(
|
||||
"Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6.",
|
||||
),
|
||||
).toBe("guard_blocked");
|
||||
});
|
||||
});
|
||||
59
src/agents/pi-embedded-runner/compact-reasons.ts
Normal file
59
src/agents/pi-embedded-runner/compact-reasons.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
function isGenericCompactionCancelledReason(reason: string): boolean {
|
||||
const normalized = reason.trim().toLowerCase();
|
||||
return normalized === "compaction cancelled" || normalized === "error: compaction cancelled";
|
||||
}
|
||||
|
||||
export function resolveCompactionFailureReason(params: {
|
||||
reason: string;
|
||||
safeguardCancelReason?: string | null;
|
||||
}): string {
|
||||
if (isGenericCompactionCancelledReason(params.reason) && params.safeguardCancelReason) {
|
||||
return params.safeguardCancelReason;
|
||||
}
|
||||
return params.reason;
|
||||
}
|
||||
|
||||
export function classifyCompactionReason(reason?: string): string {
|
||||
const text = (reason ?? "").trim().toLowerCase();
|
||||
if (!text) {
|
||||
return "unknown";
|
||||
}
|
||||
if (text.includes("nothing to compact")) {
|
||||
return "no_compactable_entries";
|
||||
}
|
||||
if (text.includes("below threshold")) {
|
||||
return "below_threshold";
|
||||
}
|
||||
if (text.includes("already compacted")) {
|
||||
return "already_compacted_recently";
|
||||
}
|
||||
if (text.includes("still exceeds target")) {
|
||||
return "live_context_still_exceeds_target";
|
||||
}
|
||||
if (text.includes("guard")) {
|
||||
return "guard_blocked";
|
||||
}
|
||||
if (text.includes("summary")) {
|
||||
return "summary_failed";
|
||||
}
|
||||
if (text.includes("timed out") || text.includes("timeout")) {
|
||||
return "timeout";
|
||||
}
|
||||
if (
|
||||
text.includes("400") ||
|
||||
text.includes("401") ||
|
||||
text.includes("403") ||
|
||||
text.includes("429")
|
||||
) {
|
||||
return "provider_error_4xx";
|
||||
}
|
||||
if (
|
||||
text.includes("500") ||
|
||||
text.includes("502") ||
|
||||
text.includes("503") ||
|
||||
text.includes("504")
|
||||
) {
|
||||
return "provider_error_5xx";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
@@ -64,6 +64,10 @@ import {
|
||||
validateAnthropicTurns,
|
||||
validateGeminiTurns,
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import {
|
||||
consumeCompactionSafeguardCancelReason,
|
||||
setCompactionSafeguardCancelReason,
|
||||
} from "../pi-extensions/compaction-safeguard-runtime.js";
|
||||
import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js";
|
||||
import { createOpenClawCodingTools } from "../pi-tools.js";
|
||||
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
||||
@@ -83,6 +87,7 @@ import {
|
||||
type SkillSnapshot,
|
||||
} from "../skills.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { classifyCompactionReason, resolveCompactionFailureReason } from "./compact-reasons.js";
|
||||
import {
|
||||
compactWithSafetyTimeout,
|
||||
resolveCompactionTimeoutMs,
|
||||
@@ -253,51 +258,6 @@ function summarizeCompactionMessages(messages: AgentMessage[]): CompactionMessag
|
||||
};
|
||||
}
|
||||
|
||||
function classifyCompactionReason(reason?: string): string {
|
||||
const text = (reason ?? "").trim().toLowerCase();
|
||||
if (!text) {
|
||||
return "unknown";
|
||||
}
|
||||
if (text.includes("nothing to compact")) {
|
||||
return "no_compactable_entries";
|
||||
}
|
||||
if (text.includes("below threshold")) {
|
||||
return "below_threshold";
|
||||
}
|
||||
if (text.includes("already compacted")) {
|
||||
return "already_compacted_recently";
|
||||
}
|
||||
if (text.includes("still exceeds target")) {
|
||||
return "live_context_still_exceeds_target";
|
||||
}
|
||||
if (text.includes("guard")) {
|
||||
return "guard_blocked";
|
||||
}
|
||||
if (text.includes("summary")) {
|
||||
return "summary_failed";
|
||||
}
|
||||
if (text.includes("timed out") || text.includes("timeout")) {
|
||||
return "timeout";
|
||||
}
|
||||
if (
|
||||
text.includes("400") ||
|
||||
text.includes("401") ||
|
||||
text.includes("403") ||
|
||||
text.includes("429")
|
||||
) {
|
||||
return "provider_error_4xx";
|
||||
}
|
||||
if (
|
||||
text.includes("500") ||
|
||||
text.includes("502") ||
|
||||
text.includes("503") ||
|
||||
text.includes("504")
|
||||
) {
|
||||
return "provider_error_5xx";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function resolvePostCompactionIndexSyncMode(config?: OpenClawConfig): "off" | "async" | "await" {
|
||||
const mode = config?.agents?.defaults?.compaction?.postIndexSync;
|
||||
if (mode === "off" || mode === "async" || mode === "await") {
|
||||
@@ -741,6 +701,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
});
|
||||
|
||||
let restoreSkillEnv: (() => void) | undefined;
|
||||
let compactionSessionManager: unknown = null;
|
||||
try {
|
||||
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
@@ -1004,6 +965,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
allowSyntheticToolResults: transcriptPolicy.allowSyntheticToolResults,
|
||||
allowedToolNames,
|
||||
});
|
||||
compactionSessionManager = sessionManager;
|
||||
trackSessionManagerAccess(params.sessionFile);
|
||||
const settingsManager = createPreparedEmbeddedPiSettingsManager({
|
||||
cwd: effectiveWorkspace,
|
||||
@@ -1162,7 +1124,10 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
// the sanity check below becomes a no-op instead of crashing compaction.
|
||||
}
|
||||
const result = await compactWithSafetyTimeout(
|
||||
() => session.compact(params.customInstructions),
|
||||
() => {
|
||||
setCompactionSafeguardCancelReason(compactionSessionManager, undefined);
|
||||
return session.compact(params.customInstructions);
|
||||
},
|
||||
compactionTimeoutMs,
|
||||
{
|
||||
abortSignal: params.abortSignal,
|
||||
@@ -1260,7 +1225,10 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
await sessionLock.release();
|
||||
}
|
||||
} catch (err) {
|
||||
const reason = describeUnknownError(err);
|
||||
const reason = resolveCompactionFailureReason({
|
||||
reason: describeUnknownError(err),
|
||||
safeguardCancelReason: consumeCompactionSafeguardCancelReason(compactionSessionManager),
|
||||
});
|
||||
return fail(reason);
|
||||
} finally {
|
||||
restoreSkillEnv?.();
|
||||
|
||||
@@ -17,6 +17,12 @@ export type CompactionSafeguardRuntimeValue = {
|
||||
recentTurnsPreserve?: number;
|
||||
qualityGuardEnabled?: boolean;
|
||||
qualityGuardMaxRetries?: number;
|
||||
/**
|
||||
* Pending human-readable cancel reason from the current safeguard compaction
|
||||
* attempt. OpenClaw consumes this to replace the upstream generic
|
||||
* "Compaction cancelled" message.
|
||||
*/
|
||||
cancelReason?: string;
|
||||
};
|
||||
|
||||
const registry = createSessionManagerRuntimeRegistry<CompactionSafeguardRuntimeValue>();
|
||||
@@ -24,3 +30,40 @@ const registry = createSessionManagerRuntimeRegistry<CompactionSafeguardRuntimeV
|
||||
export const setCompactionSafeguardRuntime = registry.set;
|
||||
|
||||
export const getCompactionSafeguardRuntime = registry.get;
|
||||
|
||||
export function setCompactionSafeguardCancelReason(
|
||||
sessionManager: unknown,
|
||||
reason: string | undefined,
|
||||
): void {
|
||||
const current = getCompactionSafeguardRuntime(sessionManager);
|
||||
const trimmed = reason?.trim();
|
||||
|
||||
if (!current) {
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
setCompactionSafeguardRuntime(sessionManager, { cancelReason: trimmed });
|
||||
return;
|
||||
}
|
||||
|
||||
const next = { ...current };
|
||||
if (trimmed) {
|
||||
next.cancelReason = trimmed;
|
||||
} else {
|
||||
delete next.cancelReason;
|
||||
}
|
||||
setCompactionSafeguardRuntime(sessionManager, next);
|
||||
}
|
||||
|
||||
export function consumeCompactionSafeguardCancelReason(sessionManager: unknown): string | null {
|
||||
const current = getCompactionSafeguardRuntime(sessionManager);
|
||||
const reason = current?.cancelReason?.trim();
|
||||
if (!reason) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const next = { ...current };
|
||||
delete next.cancelReason;
|
||||
setCompactionSafeguardRuntime(sessionManager, Object.keys(next).length > 0 ? next : null);
|
||||
return reason;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import * as compactionModule from "../compaction.js";
|
||||
import { buildEmbeddedExtensionFactories } from "../pi-embedded-runner/extensions.js";
|
||||
import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js";
|
||||
import {
|
||||
consumeCompactionSafeguardCancelReason,
|
||||
getCompactionSafeguardRuntime,
|
||||
setCompactionSafeguardCancelReason,
|
||||
setCompactionSafeguardRuntime,
|
||||
} from "./compaction-safeguard-runtime.js";
|
||||
import compactionSafeguardExtension, { __testing } from "./compaction-safeguard.js";
|
||||
@@ -539,6 +541,24 @@ describe("compaction-safeguard runtime registry", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("consumes cancel reasons without dropping other runtime fields", () => {
|
||||
const sm = {};
|
||||
setCompactionSafeguardRuntime(sm, { maxHistoryShare: 0.6 });
|
||||
setCompactionSafeguardCancelReason(sm, "no API key");
|
||||
|
||||
expect(consumeCompactionSafeguardCancelReason(sm)).toBe("no API key");
|
||||
expect(consumeCompactionSafeguardCancelReason(sm)).toBeNull();
|
||||
expect(getCompactionSafeguardRuntime(sm)).toEqual({ maxHistoryShare: 0.6 });
|
||||
});
|
||||
|
||||
it("clears cancel reason when set to undefined", () => {
|
||||
const sm = {};
|
||||
setCompactionSafeguardCancelReason(sm, "temporary reason");
|
||||
expect(consumeCompactionSafeguardCancelReason(sm)).toBe("temporary reason");
|
||||
setCompactionSafeguardCancelReason(sm, undefined);
|
||||
expect(consumeCompactionSafeguardCancelReason(sm)).toBeNull();
|
||||
});
|
||||
|
||||
it("wires oversized safeguard runtime values when config validation is bypassed", () => {
|
||||
const sessionManager = {} as unknown as Parameters<
|
||||
typeof buildEmbeddedExtensionFactories
|
||||
|
||||
@@ -31,7 +31,10 @@ import {
|
||||
composeSplitTurnInstructions,
|
||||
resolveCompactionInstructions,
|
||||
} from "./compaction-instructions.js";
|
||||
import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js";
|
||||
import {
|
||||
getCompactionSafeguardRuntime,
|
||||
setCompactionSafeguardCancelReason,
|
||||
} from "./compaction-safeguard-runtime.js";
|
||||
|
||||
const log = createSubsystemLogger("compaction-safeguard");
|
||||
|
||||
@@ -783,6 +786,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
const hasRealTurnPrefix = preparation.turnPrefixMessages.some((message, index, messages) =>
|
||||
isRealConversationMessage(message, messages, index),
|
||||
);
|
||||
setCompactionSafeguardCancelReason(ctx.sessionManager, undefined);
|
||||
if (!hasRealSummarizable && !hasRealTurnPrefix) {
|
||||
// When there are no summarizable messages AND no real turn-prefix content,
|
||||
// cancelling compaction leaves context unchanged but the SDK re-triggers
|
||||
@@ -840,6 +844,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
"was not called and model was not passed through runtime registry.",
|
||||
);
|
||||
}
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
"Compaction safeguard could not resolve a summarization model.",
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
@@ -848,6 +856,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
log.warn(
|
||||
"Compaction safeguard: no API key available; cancelling compaction to preserve history.",
|
||||
);
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
`Compaction safeguard could not resolve an API key for ${model.provider}/${model.id}.`,
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
@@ -1103,10 +1115,13 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.warn(
|
||||
`Compaction summarization failed; cancelling compaction to preserve history: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
`Compaction summarization failed; cancelling compaction to preserve history: ${message}`,
|
||||
);
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
`Compaction safeguard could not summarize the session: ${message}`,
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
@@ -44,6 +44,38 @@ function extractCompactInstructions(params: {
|
||||
return rest.length ? rest : undefined;
|
||||
}
|
||||
|
||||
function isCompactionSkipReason(reason?: string): boolean {
|
||||
const text = reason?.trim().toLowerCase() ?? "";
|
||||
return (
|
||||
text.includes("nothing to compact") ||
|
||||
text.includes("below threshold") ||
|
||||
text.includes("already compacted") ||
|
||||
text.includes("no real conversation messages")
|
||||
);
|
||||
}
|
||||
|
||||
function formatCompactionReason(reason?: string): string | undefined {
|
||||
const text = reason?.trim();
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lower = text.toLowerCase();
|
||||
if (lower.includes("nothing to compact")) {
|
||||
return "nothing compactable in this session yet";
|
||||
}
|
||||
if (lower.includes("below threshold")) {
|
||||
return "context is below the compaction threshold";
|
||||
}
|
||||
if (lower.includes("already compacted")) {
|
||||
return "session was already compacted recently";
|
||||
}
|
||||
if (lower.includes("no real conversation messages")) {
|
||||
return "no real conversation messages yet";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
const compactRequested =
|
||||
params.command.commandBodyNormalized === "/compact" ||
|
||||
@@ -110,15 +142,16 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
||||
});
|
||||
|
||||
const compactLabel = result.ok
|
||||
? result.compacted
|
||||
? result.result?.tokensBefore != null && result.result?.tokensAfter != null
|
||||
? `Compacted (${formatTokenCount(result.result.tokensBefore)} → ${formatTokenCount(result.result.tokensAfter)})`
|
||||
: result.result?.tokensBefore
|
||||
? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
|
||||
: "Compacted"
|
||||
: "Compaction skipped"
|
||||
: "Compaction failed";
|
||||
const compactLabel =
|
||||
result.ok || isCompactionSkipReason(result.reason)
|
||||
? result.compacted
|
||||
? result.result?.tokensBefore != null && result.result?.tokensAfter != null
|
||||
? `Compacted (${formatTokenCount(result.result.tokensBefore)} → ${formatTokenCount(result.result.tokensAfter)})`
|
||||
: result.result?.tokensBefore
|
||||
? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
|
||||
: "Compacted"
|
||||
: "Compaction skipped"
|
||||
: "Compaction failed";
|
||||
if (result.ok && result.compacted) {
|
||||
await incrementCompactionCount({
|
||||
sessionEntry: params.sessionEntry,
|
||||
@@ -136,7 +169,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null,
|
||||
params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
|
||||
);
|
||||
const reason = result.reason?.trim();
|
||||
const reason = formatCompactionReason(result.reason);
|
||||
const line = reason
|
||||
? `${compactLabel}: ${reason} • ${contextSummary}`
|
||||
: `${compactLabel} • ${contextSummary}`;
|
||||
|
||||
@@ -120,6 +120,7 @@ const { clearPluginCommands, registerPluginCommand } = await import("../../plugi
|
||||
const { abortEmbeddedPiRun, compactEmbeddedPiSession } =
|
||||
await import("../../agents/pi-embedded.js");
|
||||
const { __testing: subagentControlTesting } = await import("../../agents/subagent-control.js");
|
||||
const { enqueueSystemEvent } = await import("../../infra/system-events.js");
|
||||
const { resetBashChatCommandForTests } = await import("./bash-command.js");
|
||||
const { handleCompactCommand } = await import("./commands-compact.js");
|
||||
const { buildCommandsPaginationKeyboard } = await import("./commands-info.js");
|
||||
@@ -625,6 +626,109 @@ describe("/compact command", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("labels nothing-to-compact results as skipped without calling them below-threshold", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/compact", cfg);
|
||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "Nothing to compact (session too small)",
|
||||
});
|
||||
|
||||
const result = await handleCompactCommand(
|
||||
{
|
||||
...params,
|
||||
sessionEntry: {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 31_000,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚙️ Compaction skipped: nothing compactable in this session yet • Context 31k/?",
|
||||
},
|
||||
});
|
||||
expect(vi.mocked(enqueueSystemEvent)).toHaveBeenCalledWith(
|
||||
"Compaction skipped: nothing compactable in this session yet • Context 31k/?",
|
||||
{ sessionKey: params.sessionKey },
|
||||
);
|
||||
});
|
||||
|
||||
it("formats below-threshold skip reasons with friendly copy", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/compact", cfg);
|
||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "Compaction skipped: below threshold for manual compaction",
|
||||
});
|
||||
|
||||
const result = await handleCompactCommand(
|
||||
{
|
||||
...params,
|
||||
sessionEntry: {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 31_000,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚙️ Compaction skipped: context is below the compaction threshold • Context 31k/?",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps true compaction errors labeled as failures", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/compact", cfg);
|
||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
reason: "Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6.",
|
||||
});
|
||||
|
||||
const result = await handleCompactCommand(
|
||||
{
|
||||
...params,
|
||||
sessionEntry: {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 109_000,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚙️ Compaction failed: Compaction safeguard could not resolve an API key for anthropic/claude-opus-4-6. • Context 109k/?",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("abort trigger command", () => {
|
||||
|
||||
@@ -203,7 +203,6 @@ describe("registerPreActionHooks", () => {
|
||||
|
||||
it("handles debug mode and plugin-required command preaction", async () => {
|
||||
const processTitleSetSpy = vi.spyOn(process, "title", "set");
|
||||
|
||||
await runPreAction({
|
||||
parseArgv: ["status"],
|
||||
processArgv: ["node", "openclaw", "status", "--debug"],
|
||||
|
||||
Reference in New Issue
Block a user