From 722c4aa0aeab181dabcaea50fcdf1cc34b5b2810 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 16 Aug 2026 11:37:13 +0000 Subject: [PATCH] fix(compaction): honor /clear reset boundary in prepareCompaction prepareCompaction walked the branch from the last compaction and ignored reset_boundary markers, so /compact (and auto-compaction) resurrected pre-/clear turns into the summary even though buildSessionContext already starts the model context after the boundary. Model reset_boundary as a first-class agent-core session entry and start the summarization window after the latest boundary, dropping the superseded pre-reset compaction summary. A boundary before the last compaction stays superseded by it. Fixes #8718 --- packages/agent/CHANGELOG.md | 4 + packages/agent/src/compaction/compaction.ts | 21 +++- packages/agent/src/compaction/entries.ts | 11 ++ .../agent/test/compact-reset-boundary.test.ts | 102 ++++++++++++++++++ .../src/session/session-entries.ts | 1 - 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/agent/test/compact-reset-boundary.test.ts diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index f47be5dbb..7ae8b7c7d 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/compact` (and automatic compaction) resurrecting pre-`/clear` conversation turns: `prepareCompaction` now honors the latest `reset_boundary`, so a compaction after an in-place `/clear` only summarizes messages created after the reset ([#8718](https://github.com/can1357/oh-my-pi/issues/8718)). + ## [17.3.5] - 2026-08-16 ### Added diff --git a/packages/agent/src/compaction/compaction.ts b/packages/agent/src/compaction/compaction.ts index 26dabbf58..3c3390320 100644 --- a/packages/agent/src/compaction/compaction.ts +++ b/packages/agent/src/compaction/compaction.ts @@ -1271,7 +1271,26 @@ export function prepareCompaction( prevCompactionIndex = i; break; } - const boundaryStart = prevCompactionIndex + 1; + + // Honor the latest `/clear` reset boundary. `/clear` records a + // `reset_boundary` marker and reports the model context empty, so compaction + // must not resurrect the dropped pre-clear turns into its summary — matching + // how buildSessionContext starts the model-context rebuild after the boundary. + // A boundary after the last reusable compaction supersedes it: the pre-reset + // summary was cleared too, so drop the previous-compaction reuse and start + // fresh after the boundary. A boundary at or before that compaction is already + // superseded by it, so only scan newer entries. + let resetBoundaryIndex = -1; + for (let i = pathEntries.length - 1; i > prevCompactionIndex; i--) { + if (pathEntries[i].type === "reset_boundary") { + resetBoundaryIndex = i; + break; + } + } + if (resetBoundaryIndex > prevCompactionIndex) { + prevCompactionIndex = -1; + } + const boundaryStart = Math.max(prevCompactionIndex, resetBoundaryIndex) + 1; const boundaryEnd = pathEntries.length; const lastUsage = getLastAssistantUsage(pathEntries); diff --git a/packages/agent/src/compaction/entries.ts b/packages/agent/src/compaction/entries.ts index 3f7ebb51e..93886763b 100644 --- a/packages/agent/src/compaction/entries.ts +++ b/packages/agent/src/compaction/entries.ts @@ -117,6 +117,16 @@ export interface ModeChangeEntry extends SessionEntryBase { data?: Record; } +/** + * Durable context-reset marker recorded by an in-place `/clear`. It carries no + * payload — its presence on the branch means every entry before it was dropped + * from the model context, so context assembly and compaction start after the + * latest one. The full pre-reset history stays on disk for transcript export. + */ +export interface ResetBoundaryEntry extends SessionEntryBase { + type: "reset_boundary"; +} + export interface CustomCompactionSessionEntries {} export type SessionEntry = @@ -133,6 +143,7 @@ export type SessionEntry = | TtsrInjectionEntry | SessionInitEntry | ModeChangeEntry + | ResetBoundaryEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries]; export interface ReadonlySessionManager { diff --git a/packages/agent/test/compact-reset-boundary.test.ts b/packages/agent/test/compact-reset-boundary.test.ts new file mode 100644 index 000000000..27faf11da --- /dev/null +++ b/packages/agent/test/compact-reset-boundary.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test } from "bun:test"; +import { + type CompactionEntry, + DEFAULT_COMPACTION_SETTINGS, + prepareCompaction, + type SessionEntry, +} from "@oh-my-pi/pi-agent-core/compaction"; +import { createAssistantMessage, createUserMessage } from "./helpers"; + +let seq = 0; +function base(type: string) { + const id = `e${seq++}`; + return { id, parentId: null, timestamp: new Date().toISOString(), type }; +} +function userEntry(text: string): SessionEntry { + return { ...base("message"), type: "message", message: createUserMessage(text) }; +} +function assistantEntry(text: string): SessionEntry { + return { + ...base("message"), + type: "message", + message: createAssistantMessage([{ type: "text", text }]), + }; +} +function resetBoundary(): SessionEntry { + return { ...base("reset_boundary"), type: "reset_boundary" }; +} +function compaction(summary: string, firstKeptEntryId: string): SessionEntry { + const entry: CompactionEntry = { + ...base("compaction"), + type: "compaction", + summary, + firstKeptEntryId, + tokensBefore: 0, + }; + return entry; +} + +describe("prepareCompaction reset boundary", () => { + test("does not resurrect pre-clear turns into the summary", () => { + const entries: SessionEntry[] = [ + userEntry("PRECLEAR user request"), + assistantEntry("PRECLEAR assistant answer"), + resetBoundary(), + userEntry("POSTCLEAR first request"), + assistantEntry("POSTCLEAR first answer"), + userEntry("POSTCLEAR second request"), + assistantEntry("POSTCLEAR second answer"), + ]; + const prep = prepareCompaction(entries, { ...DEFAULT_COMPACTION_SETTINGS, keepRecentTokens: 1 }); + expect(prep).toBeDefined(); + const summarized = JSON.stringify(prep?.messagesToSummarize ?? []); + const kept = JSON.stringify([...(prep?.turnPrefixMessages ?? []), ...(prep?.recentMessages ?? [])]); + expect(summarized).not.toContain("PRECLEAR"); + expect(kept).not.toContain("PRECLEAR"); + expect(summarized).toContain("POSTCLEAR first"); + }); + + test("a reset boundary after the last compaction drops that compaction's summary reuse", () => { + const entries: SessionEntry[] = [ + userEntry("OLD user"), + assistantEntry("OLD assistant"), + compaction("OLD SUMMARY", "kept-old"), + userEntry("MIDCLEAR user"), + assistantEntry("MIDCLEAR assistant"), + resetBoundary(), + userEntry("POST first"), + assistantEntry("POST first answer"), + userEntry("POST second"), + assistantEntry("POST second answer"), + ]; + const prep = prepareCompaction(entries, { ...DEFAULT_COMPACTION_SETTINGS, keepRecentTokens: 1 }); + expect(prep).toBeDefined(); + // The cleared compaction is not reused as previous context. + expect(prep?.previousSummary).toBeUndefined(); + const summarized = JSON.stringify(prep?.messagesToSummarize ?? []); + expect(summarized).not.toContain("MIDCLEAR"); + expect(summarized).not.toContain("OLD"); + expect(summarized).toContain("POST first"); + }); + + test("a reset boundary before the last compaction is superseded by it", () => { + const entries: SessionEntry[] = [ + userEntry("PRE user"), + resetBoundary(), + userEntry("MID user"), + assistantEntry("MID assistant"), + compaction("KEEP SUMMARY", "kept-mid"), + userEntry("TAIL one"), + assistantEntry("TAIL one answer"), + userEntry("TAIL two"), + assistantEntry("TAIL two answer"), + ]; + const prep = prepareCompaction(entries, { ...DEFAULT_COMPACTION_SETTINGS, keepRecentTokens: 1 }); + expect(prep).toBeDefined(); + // Compaction after the boundary wins: its summary is still reused. + expect(prep?.previousSummary).toBe("KEEP SUMMARY"); + const summarized = JSON.stringify(prep?.messagesToSummarize ?? []); + expect(summarized).toContain("TAIL one"); + expect(summarized).not.toContain("MID"); + }); +}); diff --git a/packages/coding-agent/src/session/session-entries.ts b/packages/coding-agent/src/session/session-entries.ts index dc5d95b5a..ebe2c50de 100644 --- a/packages/coding-agent/src/session/session-entries.ts +++ b/packages/coding-agent/src/session/session-entries.ts @@ -171,7 +171,6 @@ declare module "@oh-my-pi/pi-agent-core/compaction/entries" { interface CustomCompactionSessionEntries { titleChange: TitleChangeEntry; credentialPin: CredentialPinEntry; - resetBoundary: ResetBoundaryEntry; } }