Skip to content

Commit 7920f8d

Browse files
committed
fix(compaction): honor manual keepRecentTokens
1 parent 92b17af commit 7920f8d

15 files changed

Lines changed: 210 additions & 12 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
6767

6868
### Fixes
6969

70+
- 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.
7071
- Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago.
7172
- 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.
7273
- 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.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
8f23e853ccde6cd021b84b32fe205f456f8516667683d16c9b56d6598f608989 config-baseline.json
2-
037bf4a873587adb8349f531c0ad79cd4f90e01712f5aa5d8b4387be73538a7f config-baseline.core.json
1+
71ef32b7723f64d4a84ac43bb6d41ff21e0d77a099b42e026d8b0d3d5301f917 config-baseline.json
2+
cfab1910132ed23777005e0c650a13f44626b0450963f733e9de56a13323ae2b config-baseline.core.json
33
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
44
86f615b7d267b03888af0af7ccb3f8232a6b636f8a741d522ff425e46729ba81 config-baseline.plugin.json

docs/concepts/compaction.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ the summary:
113113
/compact Focus on the API design decisions
114114
```
115115

116+
When `agents.defaults.compaction.keepRecentTokens` is set, manual compaction
117+
honors that Pi cut-point and keeps the recent tail in rebuilt context. Without
118+
an explicit keep budget, manual compaction behaves as a hard checkpoint and
119+
continues from the new summary alone.
120+
116121
## Using a different model
117122

118123
By default, compaction uses your agent's primary model. You can use a more

docs/gateway/config-agents.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,10 @@ Periodic heartbeat runs.
542542
provider: "my-provider", // id of a registered compaction provider plugin (optional)
543543
timeoutSeconds: 900,
544544
reserveTokensFloor: 24000,
545+
keepRecentTokens: 50000,
545546
identifierPolicy: "strict", // strict | off | custom
546547
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
548+
qualityGuard: { enabled: true, maxRetries: 1 },
547549
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
548550
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
549551
notifyUser: true, // send brief notices when compaction starts and completes (default: false)
@@ -562,8 +564,10 @@ Periodic heartbeat runs.
562564
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
563565
- `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).
564566
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`.
567+
- `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.
565568
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
566569
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
570+
- `qualityGuard`: retry-on-malformed-output checks for safeguard summaries. Enabled by default in safeguard mode; set `enabled: false` to skip the audit.
567571
- `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.
568572
- `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.
569573
- `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.

docs/reference/session-management-compaction.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ OpenClaw also enforces a safety floor for embedded runs:
267267
- Default floor is `20000` tokens.
268268
- Set `agents.defaults.compaction.reserveTokensFloor: 0` to disable the floor.
269269
- If it’s already higher, OpenClaw leaves it alone.
270+
- Manual `/compact` honors an explicit `agents.defaults.compaction.keepRecentTokens`
271+
and keeps Pi's recent-tail cut point. Without an explicit keep budget,
272+
manual compaction remains a hard checkpoint and rebuilt context starts from
273+
the new summary.
270274

271275
Why: leave enough headroom for multi-turn “housekeeping” (like memory writes) before compaction becomes unavoidable.
272276

@@ -283,6 +287,10 @@ Plugins can register a compaction provider via `registerCompactionProvider()` on
283287
- Setting a `provider` forces `mode: "safeguard"`.
284288
- Providers receive the same compaction instructions and identifier-preservation policy as the built-in path.
285289
- The safeguard still preserves recent-turn and split-turn suffix context after provider output.
290+
- Built-in safeguard summarization re-distills prior summaries with new messages
291+
instead of preserving the full previous summary verbatim.
292+
- Safeguard mode enables summary quality audits by default; set
293+
`qualityGuard.enabled: false` to skip retry-on-malformed-output behavior.
286294
- If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization automatically.
287295
- Abort/timeout signals are re-thrown (not swallowed) to respect caller cancellation.
288296

src/agents/pi-embedded-runner/compact.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,8 @@ export async function compactEmbeddedPiSessionDirect(
10591059
try {
10601060
const hardenedBoundary = await hardenManualCompactionBoundary({
10611061
sessionFile: params.sessionFile,
1062+
preserveRecentTail:
1063+
typeof params.config?.agents?.defaults?.compaction?.keepRecentTokens === "number",
10621064
});
10631065
if (hardenedBoundary.applied) {
10641066
effectiveFirstKeptEntryId =

src/agents/pi-embedded-runner/extensions.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function expectSafeguardRuntime(
4545
}
4646

4747
describe("buildEmbeddedExtensionFactories", () => {
48-
it("does not opt safeguard mode into quality-guard retries", () => {
48+
it("enables quality-guard retries by default in safeguard mode", () => {
4949
const cfg = {
5050
agents: {
5151
defaults: {
@@ -55,6 +55,24 @@ describe("buildEmbeddedExtensionFactories", () => {
5555
},
5656
},
5757
} as OpenClawConfig;
58+
expectSafeguardRuntime(cfg, {
59+
qualityGuardEnabled: true,
60+
});
61+
});
62+
63+
it("honors explicit safeguard quality-guard disablement", () => {
64+
const cfg = {
65+
agents: {
66+
defaults: {
67+
compaction: {
68+
mode: "safeguard",
69+
qualityGuard: {
70+
enabled: false,
71+
},
72+
},
73+
},
74+
},
75+
} as OpenClawConfig;
5876
expectSafeguardRuntime(cfg, {
5977
qualityGuardEnabled: false,
6078
});

src/agents/pi-embedded-runner/extensions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export function buildEmbeddedExtensionFactories(params: {
162162
identifierPolicy: compactionCfg?.identifierPolicy,
163163
identifierInstructions: compactionCfg?.identifierInstructions,
164164
customInstructions: compactionCfg?.customInstructions,
165-
qualityGuardEnabled: qualityGuardCfg?.enabled ?? false,
165+
qualityGuardEnabled: qualityGuardCfg?.enabled ?? true,
166166
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
167167
model: params.model,
168168
recentTurnsPreserve: compactionCfg?.recentTurnsPreserve,

src/agents/pi-embedded-runner/manual-compaction-boundary.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,39 @@ describe("hardenManualCompactionBoundary", () => {
118118
expect(afterTexts.join("\n")).not.toContain("detailed new answer");
119119
});
120120

121+
it("keeps the upstream recent tail when requested", async () => {
122+
const dir = await makeTmpDir();
123+
const session = SessionManager.create(dir, dir);
124+
125+
session.appendMessage({ role: "user", content: "old question", timestamp: 1 });
126+
session.appendMessage(createAssistantTextMessage("old answer", 2));
127+
const keepId = session.getBranch().at(-1)?.id;
128+
expect(keepId).toBeTruthy();
129+
const latestCompactionId = session.appendCompaction("fresh summary", keepId!, 200);
130+
const sessionFile = session.getSessionFile();
131+
expect(sessionFile).toBeTruthy();
132+
133+
const hardened = await hardenManualCompactionBoundary({
134+
sessionFile: sessionFile!,
135+
preserveRecentTail: true,
136+
});
137+
expect(hardened.applied).toBe(false);
138+
expect(hardened.firstKeptEntryId).toBe(keepId);
139+
140+
const reopened = SessionManager.open(sessionFile!);
141+
const latest = reopened.getLeafEntry();
142+
expect(latest?.type).toBe("compaction");
143+
if (!latest || latest.type !== "compaction") {
144+
throw new Error("expected latest leaf to be a compaction entry");
145+
}
146+
expect(latest.id).toBe(latestCompactionId);
147+
expect(latest.firstKeptEntryId).toBe(keepId);
148+
expect(reopened.buildSessionContext().messages.map((message) => message.role)).toEqual([
149+
"compactionSummary",
150+
"assistant",
151+
]);
152+
});
153+
121154
it("is a no-op when the latest leaf is not a compaction entry", async () => {
122155
const dir = await makeTmpDir();
123156
const session = SessionManager.create(dir, dir);

src/agents/pi-embedded-runner/manual-compaction-boundary.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function replaceLatestCompactionBoundary(params: {
4040

4141
export async function hardenManualCompactionBoundary(params: {
4242
sessionFile: string;
43+
preserveRecentTail?: boolean;
4344
}): Promise<HardenedManualCompactionBoundary> {
4445
const sessionManager = SessionManager.open(params.sessionFile) as Partial<SessionManagerLike>;
4546
if (
@@ -68,6 +69,19 @@ export async function hardenManualCompactionBoundary(params: {
6869
};
6970
}
7071

72+
if (params.preserveRecentTail) {
73+
const sessionContext = sessionManager.buildSessionContext();
74+
return {
75+
applied: false,
76+
firstKeptEntryId: leaf.firstKeptEntryId,
77+
leafId:
78+
typeof sessionManager.getLeafId === "function"
79+
? (sessionManager.getLeafId() ?? undefined)
80+
: undefined,
81+
messages: sessionContext.messages,
82+
};
83+
}
84+
7185
if (leaf.firstKeptEntryId === leaf.id) {
7286
const sessionContext = sessionManager.buildSessionContext();
7387
return {

0 commit comments

Comments
 (0)