From 5b9b70d29cdfa3e71b45c26698c2226c21d762f9 Mon Sep 17 00:00:00 2001 From: Enrico <5575513+PizzaMarinara@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:36:16 +0200 Subject: [PATCH] feat(coding-agent): add compaction reason and willRetry to extension compact events (#5962) session_before_compact and session_compact now carry reason ("manual" | "threshold" | "overflow") and willRetry, matching what the RPC protocol already exposes via auto_compaction_start/end. Extensions can now distinguish manual /compact from threshold auto-compaction and overflow recovery through the public API instead of monkeypatching AgentSession internals. --- packages/coding-agent/docs/compaction.md | 4 +- packages/coding-agent/docs/extensions.md | 7 +- .../coding-agent/src/core/agent-session.ts | 8 ++ .../coding-agent/src/core/extensions/types.ts | 8 ++ .../5217-compaction-reason.test.ts | 95 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index babcddc6..5e0d4eef 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -276,7 +276,7 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; // preparation.messagesToSummarize - messages to summarize // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn) @@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => { // preparation.settings - compaction settings // branchEntries - all entries on current branch (for custom state) + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // signal - AbortSignal (pass to LLM calls) // Cancel: diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 5a559813..a9271ee8 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details. ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; + + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // Cancel: return { cancel: true }; @@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => { pi.on("session_compact", async (event, ctx) => { // event.compactionEntry - the saved compaction // event.fromExtension - whether extension provided it + // event.reason - "manual" (/compact), "threshold", or "overflow" + // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery) }); ``` diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index a1033128..395d8161 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1684,6 +1684,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions, + reason: "manual", + willRetry: false, signal: this._compactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -1747,6 +1749,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason: "manual", + willRetry: false, }); } @@ -1945,6 +1949,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions: undefined, + reason, + willRetry, signal: this._autoCompactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -2022,6 +2028,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason, + willRetry, }); } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index a869a55d..7234d4e4 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent { preparation: CompactionPreparation; branchEntries: SessionEntry[]; customInstructions?: string; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; signal: AbortSignal; } @@ -579,6 +583,10 @@ export interface SessionCompactEvent { type: "session_compact"; compactionEntry: CompactionEntry; fromExtension: boolean; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; } /** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ diff --git a/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts new file mode 100644 index 00000000..9af206fe --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts @@ -0,0 +1,95 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionFactory } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +type SessionWithCompactionInternals = { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; +}; + +interface RecordedCompactionEvent { + type: "session_before_compact" | "session_compact"; + reason: "manual" | "threshold" | "overflow"; + willRetry: boolean; +} + +function recordingExtension(recorded: RecordedCompactionEvent[]): ExtensionFactory { + return (pi) => { + pi.on("session_before_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + return { + compaction: { + summary: "summary from extension", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + pi.on("session_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + }); + }; +} + +async function createCompactionHarness(recorded: RecordedCompactionEvent[]): Promise { + const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, + extensionFactories: [recordingExtension(recorded)], + }); + harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]); + await harness.session.prompt("first"); + await harness.session.prompt("second"); + return harness; +} + +describe("issue #5217 compaction reason on extension events", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("reports manual reason for compact()", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + + await harness.session.compact(); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "manual", willRetry: false }, + { type: "session_compact", reason: "manual", willRetry: false }, + ]); + }); + + it("reports threshold reason for auto-compaction", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("threshold", false); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "threshold", willRetry: false }, + { type: "session_compact", reason: "threshold", willRetry: false }, + ]); + }); + + it("reports overflow reason and willRetry for overflow recovery", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("overflow", true); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "overflow", willRetry: true }, + { type: "session_compact", reason: "overflow", willRetry: true }, + ]); + }); +});