feat(coding-agent): added snapcompact safety checks and UI integration
- Added validation to scan for non-ASCII characters before performing snap-compaction, falling back to LLM-based summarization if the unrenderable ratio is too high. - Updated event handling and status reporting to explicitly support snapcompact actions, including specific error warnings and cancellation states in the UI. - Updated session logic to default to snapcompact strategy when auto-compaction is enabled.
This commit is contained in:
@@ -952,7 +952,9 @@ export class EventController {
|
||||
? "Auto-handoff"
|
||||
: event.action === "shake"
|
||||
? "Auto-shake"
|
||||
: "Auto context-full maintenance";
|
||||
: event.action === "snapcompact"
|
||||
? "Auto-snapcompact"
|
||||
: "Auto context-full maintenance";
|
||||
this.ctx.autoCompactionLoader = new Loader(
|
||||
this.ctx.ui,
|
||||
spinner => theme.fg("accent", spinner),
|
||||
@@ -973,13 +975,16 @@ export class EventController {
|
||||
}
|
||||
const isHandoffAction = event.action === "handoff";
|
||||
const isShakeAction = event.action === "shake";
|
||||
const isSnapcompactAction = event.action === "snapcompact";
|
||||
if (event.aborted) {
|
||||
this.ctx.showStatus(
|
||||
isHandoffAction
|
||||
? "Auto-handoff cancelled"
|
||||
: isShakeAction
|
||||
? "Auto-shake cancelled"
|
||||
: "Auto context-full maintenance cancelled",
|
||||
: isSnapcompactAction
|
||||
? "Auto-snapcompact cancelled"
|
||||
: "Auto context-full maintenance cancelled",
|
||||
);
|
||||
} else if (isShakeAction) {
|
||||
// Shake produces no CompactionResult; rebuild on success, suppress benign skips.
|
||||
@@ -1018,6 +1023,8 @@ export class EventController {
|
||||
} else if (event.skipped) {
|
||||
// Benign skip: no model selected, no candidate models available, or nothing
|
||||
// to compact yet. Not a failure — suppress the warning.
|
||||
} else if (isSnapcompactAction) {
|
||||
this.ctx.showWarning("Auto-snapcompact maintenance failed; continuing without maintenance");
|
||||
} else {
|
||||
this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ import {
|
||||
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
||||
import type { Settings, SkillsSettings } from "../config/settings";
|
||||
import { onAppendOnlyModeChanged } from "../config/settings";
|
||||
import { getDefault, onAppendOnlyModeChanged } from "../config/settings";
|
||||
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
||||
import { loadCapability } from "../discovery";
|
||||
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
||||
@@ -9654,29 +9654,50 @@ export class AgentSession {
|
||||
// summarizer path.
|
||||
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
||||
if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
|
||||
snapcompactResult = await snapcompact.compact(preparation, {
|
||||
convertToLlm,
|
||||
model: this.model,
|
||||
});
|
||||
const ctxWindow = this.model?.contextWindow ?? 0;
|
||||
const budget =
|
||||
ctxWindow > 0
|
||||
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
||||
if (projected > budget) {
|
||||
logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
|
||||
const text = snapcompact.serializeConversation(
|
||||
convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
|
||||
);
|
||||
const renderScan = snapcompact.scanRenderability(text);
|
||||
if (renderScan.isSafe) {
|
||||
snapcompactResult = await snapcompact.compact(preparation, {
|
||||
convertToLlm,
|
||||
model: this.model,
|
||||
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
||||
});
|
||||
} else {
|
||||
logger.warn("Snapcompact disabled: high non-ASCII rate detected; falling back to an LLM summary", {
|
||||
model: this.model?.id,
|
||||
projected,
|
||||
budget,
|
||||
unrenderableRatio: renderScan.unrenderableRatio,
|
||||
});
|
||||
this.emitNotice(
|
||||
"warning",
|
||||
"snapcompact could not bring the context under the limit — using an LLM summary instead",
|
||||
`snapcompact disabled: high non-ASCII rate detected (${(renderScan.unrenderableRatio * 100).toFixed(1)}%). Falling back to an LLM summary to prevent data loss.`,
|
||||
"compaction",
|
||||
);
|
||||
action = "context-full";
|
||||
snapcompactResult = undefined;
|
||||
}
|
||||
|
||||
if (snapcompactResult) {
|
||||
const ctxWindow = this.model?.contextWindow ?? 0;
|
||||
const budget =
|
||||
ctxWindow > 0
|
||||
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
||||
if (projected > budget) {
|
||||
logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
|
||||
model: this.model?.id,
|
||||
projected,
|
||||
budget,
|
||||
});
|
||||
this.emitNotice(
|
||||
"warning",
|
||||
"snapcompact could not bring the context under the limit — using an LLM summary instead",
|
||||
"compaction",
|
||||
);
|
||||
action = "context-full";
|
||||
snapcompactResult = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10081,7 +10102,8 @@ export class AgentSession {
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
errorMessage: `Auto-shake failed: ${message}`,
|
||||
errorMessage: message,
|
||||
skipped: false,
|
||||
});
|
||||
// Overflow still needs recovery even if shake threw.
|
||||
return reason === "overflow" ? "fallback" : COMPACTION_CHECK_NONE;
|
||||
|
||||
@@ -323,7 +323,7 @@ describe("ACP lazy startup", () => {
|
||||
enabled: false,
|
||||
subagents: false,
|
||||
syncBacklog: "off",
|
||||
immuneTurns: 1,
|
||||
immuneTurns: 3,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -871,13 +871,13 @@ describe("AgentSession handoff", () => {
|
||||
expect(events.filter(event => event.type === "auto_compaction_end")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("restores context-full strategy when enabling auto-compaction from off strategy", () => {
|
||||
it("restores default strategy when enabling auto-compaction from off strategy", () => {
|
||||
session.settings.set("compaction.enabled", true);
|
||||
session.settings.set("compaction.strategy", "off");
|
||||
|
||||
expect(session.autoCompactionEnabled).toBe(false);
|
||||
session.setAutoCompactionEnabled(true);
|
||||
expect(session.settings.get("compaction.strategy")).toBe("context-full");
|
||||
expect(session.settings.get("compaction.strategy")).toBe("snapcompact");
|
||||
expect(session.autoCompactionEnabled).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user