fix: preserve unknown compaction failure detail

This commit is contained in:
Peter Steinberger
2026-04-28 05:04:33 +01:00
parent 34a0a9fd06
commit 714f3b59cc
4 changed files with 57 additions and 3 deletions

View File

@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
- Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.
- Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip `InteractionEventListener` listener timeouts. Fixes #73204. Thanks @slideshow-dingo.
- Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.
- Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc.
- Heartbeat/models: show heartbeat model bleed guidance on context-overflow resets when the last runtime model matches configured `heartbeat.model`, so smaller local heartbeat models point users to `isolatedSession` or `lightContext` instead of only compaction-buffer tuning. Fixes #67314. Thanks @Knightmare6890.
- Subagents/models: persist `sessions_spawn.model` and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99.

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { classifyCompactionReason, resolveCompactionFailureReason } from "./compact-reasons.js";
import {
classifyCompactionReason,
formatUnknownCompactionReasonDetail,
resolveCompactionFailureReason,
} from "./compact-reasons.js";
describe("resolveCompactionFailureReason", () => {
it("replaces generic compaction cancellation with the safeguard reason", () => {
@@ -37,4 +41,30 @@ describe("classifyCompactionReason", () => {
),
).toBe("guard_blocked");
});
it("keeps unclassified provider errors in the stable unknown bucket", () => {
expect(classifyCompactionReason("No API provider registered for api: ollama")).toBe("unknown");
});
});
describe("formatUnknownCompactionReasonDetail", () => {
it("formats unknown reasons as single-token diagnostic detail", () => {
expect(formatUnknownCompactionReasonDetail("No API provider registered for api: ollama")).toBe(
"No_API_provider_registered_for_api:_ollama",
);
});
it("strips terminal escapes and log separators from unknown reasons", () => {
expect(
formatUnknownCompactionReasonDetail("\u001b[31mNo API\u001b[0m provider = ollama\nnext"),
).toBe("No_API_provider_ollama_next");
});
it("omits empty unknown reason detail", () => {
expect(formatUnknownCompactionReasonDetail(" \n\t ")).toBeUndefined();
});
it("limits unknown reason detail length", () => {
expect(formatUnknownCompactionReasonDetail("x".repeat(120))).toHaveLength(100);
});
});

View File

@@ -1,4 +1,7 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { sanitizeForLog } from "../../terminal/ansi.js";
const MAX_COMPACTION_REASON_DETAIL_CHARS = 100;
function isGenericCompactionCancelledReason(reason: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(reason);
@@ -59,3 +62,15 @@ export function classifyCompactionReason(reason?: string): string {
}
return "unknown";
}
export function formatUnknownCompactionReasonDetail(reason?: string): string | undefined {
const sanitized = sanitizeForLog((reason ?? "").replace(/\s+/g, " "))
.trim()
.replace(/[^A-Za-z0-9._:@/+~-]+/g, "_")
.replace(/_+/g, "_")
.replace(/^_+|_+$/g, "");
if (!sanitized) {
return undefined;
}
return sanitized.slice(0, MAX_COMPACTION_REASON_DETAIL_CHARS);
}

View File

@@ -97,7 +97,11 @@ import {
resolveSkillsPromptForRun,
} from "../skills.js";
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
import { classifyCompactionReason, resolveCompactionFailureReason } from "./compact-reasons.js";
import {
classifyCompactionReason,
formatUnknownCompactionReasonDetail,
resolveCompactionFailureReason,
} from "./compact-reasons.js";
import type { CompactEmbeddedPiSessionParams, CompactionMessageMetrics } from "./compact.types.js";
import { dedupeDuplicateUserMessagesForCompaction } from "./compaction-duplicate-user-messages.js";
import {
@@ -351,10 +355,14 @@ export async function compactEmbeddedPiSessionDirect(
let thinkLevel: ThinkLevel = params.thinkLevel ?? "off";
const attemptedThinking = new Set<ThinkLevel>();
const fail = (reason: string): EmbeddedPiCompactResult => {
const failureReason = classifyCompactionReason(reason);
const detail =
failureReason === "unknown" ? formatUnknownCompactionReasonDetail(reason) : undefined;
const detailSuffix = detail ? ` detail=${detail}` : "";
log.warn(
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=failed reason=${classifyCompactionReason(reason)} ` +
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=failed reason=${failureReason}${detailSuffix} ` +
`durationMs=${Date.now() - startedAt}`,
);
return {