mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:20:44 +00:00
fix(compaction): honor manual keepRecentTokens
This commit is contained in:
@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Compaction: honor explicit `agents.defaults.compaction.keepRecentTokens` for manual `/compact`, re-distill safeguard summaries instead of snowballing previous summaries, and enable safeguard summary quality checks by default. Fixes #71357. Thanks @WhiteGiverMa.
|
||||||
- Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago.
|
- Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago.
|
||||||
- Browser/sandbox: pass the resolved `browser.ssrfPolicy` into sandbox browser bridges and refresh cached bridges when the effective policy changes, so sandboxed browser navigation honors private-network opt-ins. Fixes #45153 and #57055. Thanks @jzakirov, @zuoanCo, and @kybrcore.
|
- Browser/sandbox: pass the resolved `browser.ssrfPolicy` into sandbox browser bridges and refresh cached bridges when the effective policy changes, so sandboxed browser navigation honors private-network opt-ins. Fixes #45153 and #57055. Thanks @jzakirov, @zuoanCo, and @kybrcore.
|
||||||
- Browser/proxy: keep Gateway/provider proxy environment variables from proxying the OpenClaw-managed browser, so `HTTP_PROXY` and `HTTPS_PROXY` no longer block ordinary browser navigation. Fixes #71358. Thanks @Sanjays2402.
|
- Browser/proxy: keep Gateway/provider proxy environment variables from proxying the OpenClaw-managed browser, so `HTTP_PROXY` and `HTTPS_PROXY` no longer block ordinary browser navigation. Fixes #71358. Thanks @Sanjays2402.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
8f23e853ccde6cd021b84b32fe205f456f8516667683d16c9b56d6598f608989 config-baseline.json
|
71ef32b7723f64d4a84ac43bb6d41ff21e0d77a099b42e026d8b0d3d5301f917 config-baseline.json
|
||||||
037bf4a873587adb8349f531c0ad79cd4f90e01712f5aa5d8b4387be73538a7f config-baseline.core.json
|
cfab1910132ed23777005e0c650a13f44626b0450963f733e9de56a13323ae2b config-baseline.core.json
|
||||||
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
|
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
|
||||||
86f615b7d267b03888af0af7ccb3f8232a6b636f8a741d522ff425e46729ba81 config-baseline.plugin.json
|
86f615b7d267b03888af0af7ccb3f8232a6b636f8a741d522ff425e46729ba81 config-baseline.plugin.json
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ the summary:
|
|||||||
/compact Focus on the API design decisions
|
/compact Focus on the API design decisions
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `agents.defaults.compaction.keepRecentTokens` is set, manual compaction
|
||||||
|
honors that Pi cut-point and keeps the recent tail in rebuilt context. Without
|
||||||
|
an explicit keep budget, manual compaction behaves as a hard checkpoint and
|
||||||
|
continues from the new summary alone.
|
||||||
|
|
||||||
## Using a different model
|
## Using a different model
|
||||||
|
|
||||||
By default, compaction uses your agent's primary model. You can use a more
|
By default, compaction uses your agent's primary model. You can use a more
|
||||||
|
|||||||
@@ -542,8 +542,10 @@ Periodic heartbeat runs.
|
|||||||
provider: "my-provider", // id of a registered compaction provider plugin (optional)
|
provider: "my-provider", // id of a registered compaction provider plugin (optional)
|
||||||
timeoutSeconds: 900,
|
timeoutSeconds: 900,
|
||||||
reserveTokensFloor: 24000,
|
reserveTokensFloor: 24000,
|
||||||
|
keepRecentTokens: 50000,
|
||||||
identifierPolicy: "strict", // strict | off | custom
|
identifierPolicy: "strict", // strict | off | custom
|
||||||
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
||||||
|
qualityGuard: { enabled: true, maxRetries: 1 },
|
||||||
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
|
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
|
||||||
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
|
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
|
||||||
notifyUser: true, // send brief notices when compaction starts and completes (default: false)
|
notifyUser: true, // send brief notices when compaction starts and completes (default: false)
|
||||||
@@ -562,8 +564,10 @@ Periodic heartbeat runs.
|
|||||||
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
|
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
|
||||||
- `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction).
|
- `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction).
|
||||||
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`.
|
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`.
|
||||||
|
- `keepRecentTokens`: Pi cut-point budget for keeping the most recent transcript tail verbatim. Manual `/compact` honors this when explicitly set; otherwise manual compaction is a hard checkpoint.
|
||||||
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
|
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
|
||||||
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
|
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
|
||||||
|
- `qualityGuard`: retry-on-malformed-output checks for safeguard summaries. Enabled by default in safeguard mode; set `enabled: false` to skip the audit.
|
||||||
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
|
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
|
||||||
- `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model.
|
- `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model.
|
||||||
- `notifyUser`: when `true`, sends brief notices to the user when compaction starts and when it completes (for example, "Compacting context..." and "Compaction complete"). Disabled by default to keep compaction silent.
|
- `notifyUser`: when `true`, sends brief notices to the user when compaction starts and when it completes (for example, "Compacting context..." and "Compaction complete"). Disabled by default to keep compaction silent.
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ OpenClaw also enforces a safety floor for embedded runs:
|
|||||||
- Default floor is `20000` tokens.
|
- Default floor is `20000` tokens.
|
||||||
- Set `agents.defaults.compaction.reserveTokensFloor: 0` to disable the floor.
|
- Set `agents.defaults.compaction.reserveTokensFloor: 0` to disable the floor.
|
||||||
- If it’s already higher, OpenClaw leaves it alone.
|
- If it’s already higher, OpenClaw leaves it alone.
|
||||||
|
- Manual `/compact` honors an explicit `agents.defaults.compaction.keepRecentTokens`
|
||||||
|
and keeps Pi's recent-tail cut point. Without an explicit keep budget,
|
||||||
|
manual compaction remains a hard checkpoint and rebuilt context starts from
|
||||||
|
the new summary.
|
||||||
|
|
||||||
Why: leave enough headroom for multi-turn “housekeeping” (like memory writes) before compaction becomes unavoidable.
|
Why: leave enough headroom for multi-turn “housekeeping” (like memory writes) before compaction becomes unavoidable.
|
||||||
|
|
||||||
@@ -283,6 +287,10 @@ Plugins can register a compaction provider via `registerCompactionProvider()` on
|
|||||||
- Setting a `provider` forces `mode: "safeguard"`.
|
- Setting a `provider` forces `mode: "safeguard"`.
|
||||||
- Providers receive the same compaction instructions and identifier-preservation policy as the built-in path.
|
- Providers receive the same compaction instructions and identifier-preservation policy as the built-in path.
|
||||||
- The safeguard still preserves recent-turn and split-turn suffix context after provider output.
|
- The safeguard still preserves recent-turn and split-turn suffix context after provider output.
|
||||||
|
- Built-in safeguard summarization re-distills prior summaries with new messages
|
||||||
|
instead of preserving the full previous summary verbatim.
|
||||||
|
- Safeguard mode enables summary quality audits by default; set
|
||||||
|
`qualityGuard.enabled: false` to skip retry-on-malformed-output behavior.
|
||||||
- If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization automatically.
|
- If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization automatically.
|
||||||
- Abort/timeout signals are re-thrown (not swallowed) to respect caller cancellation.
|
- Abort/timeout signals are re-thrown (not swallowed) to respect caller cancellation.
|
||||||
|
|
||||||
|
|||||||
@@ -1059,6 +1059,8 @@ export async function compactEmbeddedPiSessionDirect(
|
|||||||
try {
|
try {
|
||||||
const hardenedBoundary = await hardenManualCompactionBoundary({
|
const hardenedBoundary = await hardenManualCompactionBoundary({
|
||||||
sessionFile: params.sessionFile,
|
sessionFile: params.sessionFile,
|
||||||
|
preserveRecentTail:
|
||||||
|
typeof params.config?.agents?.defaults?.compaction?.keepRecentTokens === "number",
|
||||||
});
|
});
|
||||||
if (hardenedBoundary.applied) {
|
if (hardenedBoundary.applied) {
|
||||||
effectiveFirstKeptEntryId =
|
effectiveFirstKeptEntryId =
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function expectSafeguardRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("buildEmbeddedExtensionFactories", () => {
|
describe("buildEmbeddedExtensionFactories", () => {
|
||||||
it("does not opt safeguard mode into quality-guard retries", () => {
|
it("enables quality-guard retries by default in safeguard mode", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -55,6 +55,24 @@ describe("buildEmbeddedExtensionFactories", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
|
expectSafeguardRuntime(cfg, {
|
||||||
|
qualityGuardEnabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors explicit safeguard quality-guard disablement", () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
compaction: {
|
||||||
|
mode: "safeguard",
|
||||||
|
qualityGuard: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
expectSafeguardRuntime(cfg, {
|
expectSafeguardRuntime(cfg, {
|
||||||
qualityGuardEnabled: false,
|
qualityGuardEnabled: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function buildEmbeddedExtensionFactories(params: {
|
|||||||
identifierPolicy: compactionCfg?.identifierPolicy,
|
identifierPolicy: compactionCfg?.identifierPolicy,
|
||||||
identifierInstructions: compactionCfg?.identifierInstructions,
|
identifierInstructions: compactionCfg?.identifierInstructions,
|
||||||
customInstructions: compactionCfg?.customInstructions,
|
customInstructions: compactionCfg?.customInstructions,
|
||||||
qualityGuardEnabled: qualityGuardCfg?.enabled ?? false,
|
qualityGuardEnabled: qualityGuardCfg?.enabled ?? true,
|
||||||
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
|
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
recentTurnsPreserve: compactionCfg?.recentTurnsPreserve,
|
recentTurnsPreserve: compactionCfg?.recentTurnsPreserve,
|
||||||
|
|||||||
@@ -118,6 +118,39 @@ describe("hardenManualCompactionBoundary", () => {
|
|||||||
expect(afterTexts.join("\n")).not.toContain("detailed new answer");
|
expect(afterTexts.join("\n")).not.toContain("detailed new answer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the upstream recent tail when requested", async () => {
|
||||||
|
const dir = await makeTmpDir();
|
||||||
|
const session = SessionManager.create(dir, dir);
|
||||||
|
|
||||||
|
session.appendMessage({ role: "user", content: "old question", timestamp: 1 });
|
||||||
|
session.appendMessage(createAssistantTextMessage("old answer", 2));
|
||||||
|
const keepId = session.getBranch().at(-1)?.id;
|
||||||
|
expect(keepId).toBeTruthy();
|
||||||
|
const latestCompactionId = session.appendCompaction("fresh summary", keepId!, 200);
|
||||||
|
const sessionFile = session.getSessionFile();
|
||||||
|
expect(sessionFile).toBeTruthy();
|
||||||
|
|
||||||
|
const hardened = await hardenManualCompactionBoundary({
|
||||||
|
sessionFile: sessionFile!,
|
||||||
|
preserveRecentTail: true,
|
||||||
|
});
|
||||||
|
expect(hardened.applied).toBe(false);
|
||||||
|
expect(hardened.firstKeptEntryId).toBe(keepId);
|
||||||
|
|
||||||
|
const reopened = SessionManager.open(sessionFile!);
|
||||||
|
const latest = reopened.getLeafEntry();
|
||||||
|
expect(latest?.type).toBe("compaction");
|
||||||
|
if (!latest || latest.type !== "compaction") {
|
||||||
|
throw new Error("expected latest leaf to be a compaction entry");
|
||||||
|
}
|
||||||
|
expect(latest.id).toBe(latestCompactionId);
|
||||||
|
expect(latest.firstKeptEntryId).toBe(keepId);
|
||||||
|
expect(reopened.buildSessionContext().messages.map((message) => message.role)).toEqual([
|
||||||
|
"compactionSummary",
|
||||||
|
"assistant",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("is a no-op when the latest leaf is not a compaction entry", async () => {
|
it("is a no-op when the latest leaf is not a compaction entry", async () => {
|
||||||
const dir = await makeTmpDir();
|
const dir = await makeTmpDir();
|
||||||
const session = SessionManager.create(dir, dir);
|
const session = SessionManager.create(dir, dir);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ function replaceLatestCompactionBoundary(params: {
|
|||||||
|
|
||||||
export async function hardenManualCompactionBoundary(params: {
|
export async function hardenManualCompactionBoundary(params: {
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
|
preserveRecentTail?: boolean;
|
||||||
}): Promise<HardenedManualCompactionBoundary> {
|
}): Promise<HardenedManualCompactionBoundary> {
|
||||||
const sessionManager = SessionManager.open(params.sessionFile) as Partial<SessionManagerLike>;
|
const sessionManager = SessionManager.open(params.sessionFile) as Partial<SessionManagerLike>;
|
||||||
if (
|
if (
|
||||||
@@ -68,6 +69,19 @@ export async function hardenManualCompactionBoundary(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.preserveRecentTail) {
|
||||||
|
const sessionContext = sessionManager.buildSessionContext();
|
||||||
|
return {
|
||||||
|
applied: false,
|
||||||
|
firstKeptEntryId: leaf.firstKeptEntryId,
|
||||||
|
leafId:
|
||||||
|
typeof sessionManager.getLeafId === "function"
|
||||||
|
? (sessionManager.getLeafId() ?? undefined)
|
||||||
|
: undefined,
|
||||||
|
messages: sessionContext.messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (leaf.firstKeptEntryId === leaf.id) {
|
if (leaf.firstKeptEntryId === leaf.id) {
|
||||||
const sessionContext = sessionManager.buildSessionContext();
|
const sessionContext = sessionManager.buildSessionContext();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function buildCompactionStructureInstructions(
|
|||||||
...REQUIRED_SUMMARY_SECTIONS,
|
...REQUIRED_SUMMARY_SECTIONS,
|
||||||
identifierSectionInstruction,
|
identifierSectionInstruction,
|
||||||
"Do not omit unresolved asks from the user.",
|
"Do not omit unresolved asks from the user.",
|
||||||
|
"When prior compaction summaries are present, re-distill them with new messages and remove stale duplicate detail.",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const custom = customInstructions?.trim();
|
const custom = customInstructions?.trim();
|
||||||
if (!custom) {
|
if (!custom) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const {
|
|||||||
formatPreservedTurnsSection,
|
formatPreservedTurnsSection,
|
||||||
buildCompactionStructureInstructions,
|
buildCompactionStructureInstructions,
|
||||||
buildStructuredFallbackSummary,
|
buildStructuredFallbackSummary,
|
||||||
|
prependPreviousSummaryForRedistill,
|
||||||
appendSummarySection,
|
appendSummarySection,
|
||||||
resolveRecentTurnsPreserve,
|
resolveRecentTurnsPreserve,
|
||||||
resolveQualityGuardMaxRetries,
|
resolveQualityGuardMaxRetries,
|
||||||
@@ -1198,6 +1199,20 @@ describe("compaction-safeguard recent-turn preservation", () => {
|
|||||||
expect(buildStructuredFallbackSummary(structured)).toBe(structured);
|
expect(buildStructuredFallbackSummary(structured)).toBe(structured);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("converts previous summaries into redistill input instead of update-prompt state", () => {
|
||||||
|
const messages: AgentMessage[] = [{ role: "user", content: "new context", timestamp: 1 }];
|
||||||
|
const redistillMessages = prependPreviousSummaryForRedistill({
|
||||||
|
messages,
|
||||||
|
previousSummary: "## Goal\nold duplicate summary",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(redistillMessages).toHaveLength(2);
|
||||||
|
expect(redistillMessages[0]?.role).toBe("user");
|
||||||
|
expect(JSON.stringify(redistillMessages[0])).toContain("<previous-compaction-summary>");
|
||||||
|
expect(JSON.stringify(redistillMessages[0])).toContain("Prune stale, duplicate");
|
||||||
|
expect(redistillMessages[1]).toBe(messages[0]);
|
||||||
|
});
|
||||||
|
|
||||||
it("restructures summaries with near-match headings instead of reusing them", () => {
|
it("restructures summaries with near-match headings instead of reusing them", () => {
|
||||||
const nearMatch = [
|
const nearMatch = [
|
||||||
"## Decisions",
|
"## Decisions",
|
||||||
@@ -1685,7 +1700,72 @@ describe("compaction-safeguard recent-turn preservation", () => {
|
|||||||
expect(summary).toContain("legacy summary without headings");
|
expect(summary).toContain("legacy summary without headings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-distills prior summaries on the LLM path instead of preserving them verbatim", async () => {
|
||||||
|
mockSummarizeInStages.mockReset();
|
||||||
|
mockSummarizeInStages.mockResolvedValue(
|
||||||
|
[
|
||||||
|
"## Decisions",
|
||||||
|
"Condensed prior context with latest status.",
|
||||||
|
"## Open TODOs",
|
||||||
|
"None.",
|
||||||
|
"## Constraints/Rules",
|
||||||
|
"Preserve identifiers.",
|
||||||
|
"## Pending user asks",
|
||||||
|
"latest ask status",
|
||||||
|
"## Exact identifiers",
|
||||||
|
"None.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionManager = stubSessionManager();
|
||||||
|
const model = createAnthropicModelFixture();
|
||||||
|
setCompactionSafeguardRuntime(sessionManager, {
|
||||||
|
model,
|
||||||
|
recentTurnsPreserve: 0,
|
||||||
|
qualityGuardEnabled: true,
|
||||||
|
qualityGuardMaxRetries: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const compactionHandler = createCompactionHandler();
|
||||||
|
const getApiKeyMock = vi.fn().mockResolvedValue("test-key");
|
||||||
|
const mockContext = createCompactionContext({
|
||||||
|
sessionManager,
|
||||||
|
getApiKeyMock,
|
||||||
|
});
|
||||||
|
const event = {
|
||||||
|
preparation: {
|
||||||
|
messagesToSummarize: [{ role: "user", content: "latest ask status", timestamp: 1 }],
|
||||||
|
turnPrefixMessages: [],
|
||||||
|
firstKeptEntryId: "entry-1",
|
||||||
|
tokensBefore: 1_500,
|
||||||
|
fileOps: {
|
||||||
|
read: [],
|
||||||
|
edited: [],
|
||||||
|
written: [],
|
||||||
|
},
|
||||||
|
settings: { reserveTokens: 4_000 },
|
||||||
|
previousSummary: "## Goal\nOld duplicated section that should be re-distilled.",
|
||||||
|
isSplitTurn: false,
|
||||||
|
},
|
||||||
|
customInstructions: "",
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = (await compactionHandler(event, mockContext)) as {
|
||||||
|
cancel?: boolean;
|
||||||
|
compaction?: { summary?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(result.cancel).not.toBe(true);
|
||||||
|
expect(mockSummarizeInStages).toHaveBeenCalledTimes(1);
|
||||||
|
const call = mockSummarizeInStages.mock.calls[0]?.[0];
|
||||||
|
expect(call?.previousSummary).toBeUndefined();
|
||||||
|
expect(JSON.stringify(call?.messages[0])).toContain("<previous-compaction-summary>");
|
||||||
|
expect(JSON.stringify(call?.messages[0])).toContain("Old duplicated section");
|
||||||
|
});
|
||||||
|
|
||||||
it("passes compaction instructions to providers and preserves suffix context", async () => {
|
it("passes compaction instructions to providers and preserves suffix context", async () => {
|
||||||
|
mockSummarizeInStages.mockReset();
|
||||||
const providerSummarize = vi.fn().mockResolvedValue("provider summary body");
|
const providerSummarize = vi.fn().mockResolvedValue("provider summary body");
|
||||||
registerCompactionProvider({
|
registerCompactionProvider({
|
||||||
id: "test-provider",
|
id: "test-provider",
|
||||||
|
|||||||
@@ -67,10 +67,37 @@ const DEFAULT_QUALITY_GUARD_MAX_RETRIES = 1;
|
|||||||
const MAX_RECENT_TURNS_PRESERVE = 12;
|
const MAX_RECENT_TURNS_PRESERVE = 12;
|
||||||
const MAX_QUALITY_GUARD_MAX_RETRIES = 3;
|
const MAX_QUALITY_GUARD_MAX_RETRIES = 3;
|
||||||
const MAX_RECENT_TURN_TEXT_CHARS = 600;
|
const MAX_RECENT_TURN_TEXT_CHARS = 600;
|
||||||
|
const PREVIOUS_SUMMARY_REDISTILL_PREFIX =
|
||||||
|
"Previous compaction summary to re-distill with the current conversation. " +
|
||||||
|
"Prune stale, duplicate, or superseded details instead of preserving it verbatim.";
|
||||||
const compactionSafeguardDeps = {
|
const compactionSafeguardDeps = {
|
||||||
summarizeInStages,
|
summarizeInStages,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildPreviousSummaryMessage(previousSummary: string): AgentMessage {
|
||||||
|
return {
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `<previous-compaction-summary>\n${PREVIOUS_SUMMARY_REDISTILL_PREFIX}\n\n${previousSummary.trim()}\n</previous-compaction-summary>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: 0,
|
||||||
|
} as AgentMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependPreviousSummaryForRedistill(params: {
|
||||||
|
messages: AgentMessage[];
|
||||||
|
previousSummary?: string;
|
||||||
|
}): AgentMessage[] {
|
||||||
|
const previousSummary = params.previousSummary?.trim();
|
||||||
|
if (!previousSummary) {
|
||||||
|
return params.messages;
|
||||||
|
}
|
||||||
|
return [buildPreviousSummaryMessage(previousSummary), ...params.messages];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt provider-based summarization. Returns the summary string on success,
|
* Attempt provider-based summarization. Returns the summary string on success,
|
||||||
* or `undefined` when the caller should fall back to built-in LLM summarization.
|
* or `undefined` when the caller should fall back to built-in LLM summarization.
|
||||||
@@ -125,8 +152,12 @@ async function summarizeViaLLM(params: {
|
|||||||
summarizationInstructions?: Parameters<typeof summarizeInStages>[0]["summarizationInstructions"];
|
summarizationInstructions?: Parameters<typeof summarizeInStages>[0]["summarizationInstructions"];
|
||||||
previousSummary?: string;
|
previousSummary?: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return compactionSafeguardDeps.summarizeInStages({
|
const messages = prependPreviousSummaryForRedistill({
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
|
previousSummary: params.previousSummary,
|
||||||
|
});
|
||||||
|
return compactionSafeguardDeps.summarizeInStages({
|
||||||
|
messages,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
apiKey: params.apiKey,
|
apiKey: params.apiKey,
|
||||||
headers: params.headers,
|
headers: params.headers,
|
||||||
@@ -136,7 +167,7 @@ async function summarizeViaLLM(params: {
|
|||||||
contextWindow: params.contextWindow,
|
contextWindow: params.contextWindow,
|
||||||
customInstructions: params.customInstructions,
|
customInstructions: params.customInstructions,
|
||||||
summarizationInstructions: params.summarizationInstructions,
|
summarizationInstructions: params.summarizationInstructions,
|
||||||
previousSummary: params.previousSummary,
|
previousSummary: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,6 +1197,7 @@ export const __testing = {
|
|||||||
formatSplitTurnContextSection,
|
formatSplitTurnContextSection,
|
||||||
buildCompactionStructureInstructions,
|
buildCompactionStructureInstructions,
|
||||||
buildStructuredFallbackSummary,
|
buildStructuredFallbackSummary,
|
||||||
|
prependPreviousSummaryForRedistill,
|
||||||
appendSummarySection,
|
appendSummarySection,
|
||||||
resolveRecentTurnsPreserve,
|
resolveRecentTurnsPreserve,
|
||||||
resolveQualityGuardMaxRetries,
|
resolveQualityGuardMaxRetries,
|
||||||
|
|||||||
@@ -4722,7 +4722,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
title: "Compaction Quality Guard Enabled",
|
title: "Compaction Quality Guard Enabled",
|
||||||
description:
|
description:
|
||||||
"Enables summary quality audits and regeneration retries for safeguard compaction. Default: false, so safeguard mode alone does not turn on retry behavior.",
|
"Enables summary quality audits and regeneration retries for safeguard compaction. Default: true in safeguard mode.",
|
||||||
},
|
},
|
||||||
maxRetries: {
|
maxRetries: {
|
||||||
type: "integer",
|
type: "integer",
|
||||||
@@ -4736,7 +4736,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
title: "Compaction Quality Guard",
|
title: "Compaction Quality Guard",
|
||||||
description:
|
description:
|
||||||
"Optional quality-audit retry settings for safeguard compaction summaries. Leave this disabled unless you explicitly want summary audits and one-shot regeneration on failed checks.",
|
"Quality-audit retry settings for safeguard compaction summaries. Safeguard mode enables this by default; set enabled: false to skip summary audits and regeneration.",
|
||||||
},
|
},
|
||||||
postIndexSync: {
|
postIndexSync: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -26066,12 +26066,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
},
|
},
|
||||||
"agents.defaults.compaction.qualityGuard": {
|
"agents.defaults.compaction.qualityGuard": {
|
||||||
label: "Compaction Quality Guard",
|
label: "Compaction Quality Guard",
|
||||||
help: "Optional quality-audit retry settings for safeguard compaction summaries. Leave this disabled unless you explicitly want summary audits and one-shot regeneration on failed checks.",
|
help: "Quality-audit retry settings for safeguard compaction summaries. Safeguard mode enables this by default; set enabled: false to skip summary audits and regeneration.",
|
||||||
tags: ["advanced"],
|
tags: ["advanced"],
|
||||||
},
|
},
|
||||||
"agents.defaults.compaction.qualityGuard.enabled": {
|
"agents.defaults.compaction.qualityGuard.enabled": {
|
||||||
label: "Compaction Quality Guard Enabled",
|
label: "Compaction Quality Guard Enabled",
|
||||||
help: "Enables summary quality audits and regeneration retries for safeguard compaction. Default: false, so safeguard mode alone does not turn on retry behavior.",
|
help: "Enables summary quality audits and regeneration retries for safeguard compaction. Default: true in safeguard mode.",
|
||||||
tags: ["advanced"],
|
tags: ["advanced"],
|
||||||
},
|
},
|
||||||
"agents.defaults.compaction.qualityGuard.maxRetries": {
|
"agents.defaults.compaction.qualityGuard.maxRetries": {
|
||||||
|
|||||||
@@ -1236,9 +1236,9 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"agents.defaults.compaction.recentTurnsPreserve":
|
"agents.defaults.compaction.recentTurnsPreserve":
|
||||||
"Number of most recent user/assistant turns kept verbatim outside safeguard summarization (default: 3). Raise this to preserve exact recent dialogue context, or lower it to maximize compaction savings.",
|
"Number of most recent user/assistant turns kept verbatim outside safeguard summarization (default: 3). Raise this to preserve exact recent dialogue context, or lower it to maximize compaction savings.",
|
||||||
"agents.defaults.compaction.qualityGuard":
|
"agents.defaults.compaction.qualityGuard":
|
||||||
"Optional quality-audit retry settings for safeguard compaction summaries. Leave this disabled unless you explicitly want summary audits and one-shot regeneration on failed checks.",
|
"Quality-audit retry settings for safeguard compaction summaries. Safeguard mode enables this by default; set enabled: false to skip summary audits and regeneration.",
|
||||||
"agents.defaults.compaction.qualityGuard.enabled":
|
"agents.defaults.compaction.qualityGuard.enabled":
|
||||||
"Enables summary quality audits and regeneration retries for safeguard compaction. Default: false, so safeguard mode alone does not turn on retry behavior.",
|
"Enables summary quality audits and regeneration retries for safeguard compaction. Default: true in safeguard mode.",
|
||||||
"agents.defaults.compaction.qualityGuard.maxRetries":
|
"agents.defaults.compaction.qualityGuard.maxRetries":
|
||||||
"Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.",
|
"Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.",
|
||||||
"agents.defaults.compaction.postIndexSync":
|
"agents.defaults.compaction.postIndexSync":
|
||||||
|
|||||||
Reference in New Issue
Block a user