fix(compaction): manual /shake keeps a recent tail of tool results

Manual /shake used protectTokens: 0, stripping every eligible tool result
including the ones the agent is still working from. Keep a small 4k-token
recent window (matching the automatic shake mechanism, at a quarter of its
16k budget) so the full escape hatch stays aggressive without destroying
the live tail. Two matcher-focused tests that implicitly relied on the
zero window now pin protectTokens: 0 explicitly.

Fixes #7776
This commit is contained in:
left-to-right
2026-08-09 18:09:17 +08:00
parent 45e12e5bb7
commit c3da093a1d
5 changed files with 26 additions and 6 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Manual `/shake` now keeps a small recent tail of tool results instead of stripping every eligible result, so the agent does not lose the context it is currently working from ([#7776](https://github.com/can1357/oh-my-pi/issues/7776)).
## [17.2.10] - 2026-08-06
### Fixed
+5 -3
View File
@@ -52,11 +52,13 @@ export const DEFAULT_SHAKE_CONFIG: ShakeConfig = {
};
/**
* Manual `/shake`: aggressive — drops every eligible region across history,
* artifact recovery reads included (the user's full escape hatch).
* Manual `/shake`: aggressive — no savings threshold and drops eligible
* regions across history, artifact recovery reads included (the user's full
* escape hatch). Still keeps a small recent tail so it cannot strip the tool
* results the agent is currently working from (#7776).
*/
export const AGGRESSIVE_SHAKE_CONFIG: ShakeConfig = {
protectTokens: 0,
protectTokens: 4_000,
minSavings: 0,
protectedTools: ["skill", isSkillReadToolResult],
fenceMinTokens: 400,
+13 -2
View File
@@ -204,12 +204,23 @@ describe("applyShakeRegions — multi-region ordering", () => {
});
describe("shake config presets", () => {
test("aggressive preset protects skill and drops everything else", () => {
expect(AGGRESSIVE_SHAKE_CONFIG.protectTokens).toBe(0);
test("aggressive preset protects skill and keeps a small recent tail", () => {
expect(AGGRESSIVE_SHAKE_CONFIG.protectTokens).toBeGreaterThan(0);
expect(AGGRESSIVE_SHAKE_CONFIG.minSavings).toBe(0);
expect(AGGRESSIVE_SHAKE_CONFIG.protectedTools).toContain("skill");
});
test("manual shake preserves the recent tool-result tail instead of stripping everything", () => {
const older = messageEntry(toolResultMessage("bash", "old-result ".repeat(300)));
const recent = messageEntry(toolResultMessage("bash", "recent-result ".repeat(3000)));
const regions = collectShakeRegions([older, recent], AGGRESSIVE_SHAKE_CONFIG);
// The recent result sits inside the preserved tail; the older one is
// still shaken aggressively.
expect(regions).toHaveLength(1);
expect(regions[0].entry).toBe(older);
});
test("default preset keeps a protect window", () => {
expect(DEFAULT_SHAKE_CONFIG.protectTokens).toBeGreaterThan(0);
expect(DEFAULT_SHAKE_CONFIG.protectedTools).toContain("skill");
+3 -1
View File
@@ -84,7 +84,9 @@ describe("conditional tool-result protection", () => {
fileResult,
];
const regions = collectShakeRegions(entries, AGGRESSIVE_SHAKE_CONFIG);
// protectTokens: 0 isolates the matcher behavior from the aggressive
// preset's recent-tail window (covered by shake.test.ts).
const regions = collectShakeRegions(entries, { ...AGGRESSIVE_SHAKE_CONFIG, protectTokens: 0 });
expect(regions).toHaveLength(1);
expect(regions[0]?.kind).toBe("toolResult");
@@ -155,6 +155,7 @@ describe("plan-read protection in compaction", () => {
const regions = collectShakeRegions(entries, {
...AGGRESSIVE_SHAKE_CONFIG,
protectTokens: 0,
protectedTools: [...AGGRESSIVE_SHAKE_CONFIG.protectedTools, matcher],
});