@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)).
|
||||||
- Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)).
|
- Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)).
|
||||||
|
|
||||||
## [0.79.7] - 2026-06-18
|
## [0.79.7] - 2026-06-18
|
||||||
|
|||||||
@@ -1892,19 +1892,10 @@ export class AgentSession {
|
|||||||
*/
|
*/
|
||||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
||||||
const settings = this.settingsManager.getCompactionSettings();
|
const settings = this.settingsManager.getCompactionSettings();
|
||||||
|
let started = false;
|
||||||
this._emit({ type: "compaction_start", reason });
|
|
||||||
this._autoCompactionAbortController = new AbortController();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.model) {
|
if (!this.model) {
|
||||||
this._emit({
|
|
||||||
type: "compaction_end",
|
|
||||||
reason,
|
|
||||||
result: undefined,
|
|
||||||
aborted: false,
|
|
||||||
willRetry: false,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1914,13 +1905,6 @@ export class AgentSession {
|
|||||||
if (this.agent.streamFn === streamSimple) {
|
if (this.agent.streamFn === streamSimple) {
|
||||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||||
if (!authResult.ok || !authResult.apiKey) {
|
if (!authResult.ok || !authResult.apiKey) {
|
||||||
this._emit({
|
|
||||||
type: "compaction_end",
|
|
||||||
reason,
|
|
||||||
result: undefined,
|
|
||||||
aborted: false,
|
|
||||||
willRetry: false,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
apiKey = authResult.apiKey;
|
apiKey = authResult.apiKey;
|
||||||
@@ -1934,16 +1918,13 @@ export class AgentSession {
|
|||||||
|
|
||||||
const preparation = prepareCompaction(pathEntries, settings);
|
const preparation = prepareCompaction(pathEntries, settings);
|
||||||
if (!preparation) {
|
if (!preparation) {
|
||||||
this._emit({
|
|
||||||
type: "compaction_end",
|
|
||||||
reason,
|
|
||||||
result: undefined,
|
|
||||||
aborted: false,
|
|
||||||
willRetry: false,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._emit({ type: "compaction_start", reason });
|
||||||
|
this._autoCompactionAbortController = new AbortController();
|
||||||
|
started = true;
|
||||||
|
|
||||||
let extensionCompaction: CompactionResult | undefined;
|
let extensionCompaction: CompactionResult | undefined;
|
||||||
let fromExtension = false;
|
let fromExtension = false;
|
||||||
|
|
||||||
@@ -2054,17 +2035,19 @@ export class AgentSession {
|
|||||||
return this.agent.hasQueuedMessages();
|
return this.agent.hasQueuedMessages();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||||
this._emit({
|
if (started) {
|
||||||
type: "compaction_end",
|
this._emit({
|
||||||
reason,
|
type: "compaction_end",
|
||||||
result: undefined,
|
reason,
|
||||||
aborted: false,
|
result: undefined,
|
||||||
willRetry: false,
|
aborted: false,
|
||||||
errorMessage:
|
willRetry: false,
|
||||||
reason === "overflow"
|
errorMessage:
|
||||||
? `Context overflow recovery failed: ${errorMessage}`
|
reason === "overflow"
|
||||||
: `Auto-compaction failed: ${errorMessage}`,
|
? `Context overflow recovery failed: ${errorMessage}`
|
||||||
});
|
: `Auto-compaction failed: ${errorMessage}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
this._autoCompactionAbortController = undefined;
|
this._autoCompactionAbortController = undefined;
|
||||||
|
|||||||
@@ -698,6 +698,10 @@ export function prepareCompaction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract file operations from messages and previous compaction
|
// Extract file operations from messages and previous compaction
|
||||||
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
||||||
|
|
||||||
|
|||||||
@@ -3700,7 +3700,6 @@ export class InteractiveMode {
|
|||||||
showError(errorMessage: string): void {
|
showError(errorMessage: string): void {
|
||||||
this.chatContainer.addChild(new Spacer(1));
|
this.chatContainer.addChild(new Spacer(1));
|
||||||
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
||||||
this.chatContainer.addChild(new Spacer(1));
|
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5695,14 +5694,6 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleCompactCommand(customInstructions?: string): Promise<void> {
|
private async handleCompactCommand(customInstructions?: string): Promise<void> {
|
||||||
const entries = this.sessionManager.getEntries();
|
|
||||||
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
||||||
|
|
||||||
if (messageCount < 2) {
|
|
||||||
this.showWarning("Nothing to compact (no messages yet)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.loadingAnimation) {
|
if (this.loadingAnimation) {
|
||||||
this.loadingAnimation.stop();
|
this.loadingAnimation.stop();
|
||||||
this.loadingAnimation = undefined;
|
this.loadingAnimation = undefined;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
|||||||
|
|
||||||
const sessionManager = SessionManager.create(tempDir);
|
const sessionManager = SessionManager.create(tempDir);
|
||||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
||||||
|
settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||||
const authStorage = AuthStorage.create(join(tempDir, "auth.json"));
|
const authStorage = AuthStorage.create(join(tempDir, "auth.json"));
|
||||||
const modelRegistry = ModelRegistry.create(authStorage);
|
const modelRegistry = ModelRegistry.create(authStorage);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
calculateContextTokens,
|
calculateContextTokens,
|
||||||
compact,
|
compact,
|
||||||
DEFAULT_COMPACTION_SETTINGS,
|
DEFAULT_COMPACTION_SETTINGS,
|
||||||
estimateContextTokens,
|
|
||||||
findCutPoint,
|
findCutPoint,
|
||||||
getLastAssistantUsage,
|
getLastAssistantUsage,
|
||||||
prepareCompaction,
|
prepareCompaction,
|
||||||
@@ -396,7 +395,7 @@ describe("buildSessionContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("prepareCompaction with previous compaction", () => {
|
describe("prepareCompaction with previous compaction", () => {
|
||||||
it("should preserve kept messages across repeated compactions when they still fit", () => {
|
it("should skip repeated compactions when kept messages still fit", () => {
|
||||||
const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)"));
|
const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)"));
|
||||||
const a1 = createMessageEntry(createAssistantMessage("assistant msg 1"));
|
const a1 = createMessageEntry(createAssistantMessage("assistant msg 1"));
|
||||||
const u2 = createMessageEntry(createUserMessage("user msg 2 - kept by compaction1"));
|
const u2 = createMessageEntry(createUserMessage("user msg 2 - kept by compaction1"));
|
||||||
@@ -408,29 +407,9 @@ describe("prepareCompaction with previous compaction", () => {
|
|||||||
const a4 = createMessageEntry(createAssistantMessage("assistant msg 4", createMockUsage(8000, 2000)));
|
const a4 = createMessageEntry(createAssistantMessage("assistant msg 4", createMockUsage(8000, 2000)));
|
||||||
|
|
||||||
const pathEntries = [u1, a1, u2, a2, u3, a3, compaction1, u4, a4];
|
const pathEntries = [u1, a1, u2, a2, u3, a3, compaction1, u4, a4];
|
||||||
const contextBefore = buildSessionContext(pathEntries);
|
|
||||||
const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS);
|
const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS);
|
||||||
|
|
||||||
expect(preparation).toBeDefined();
|
expect(preparation).toBeUndefined();
|
||||||
expect(preparation!.firstKeptEntryId).toBe(u2.id);
|
|
||||||
expect(preparation!.previousSummary).toBe("First summary");
|
|
||||||
expect(extractText(preparation!.messagesToSummarize)).not.toContain("First summary");
|
|
||||||
expect(preparation!.tokensBefore).toBe(estimateContextTokens(contextBefore.messages).tokens);
|
|
||||||
|
|
||||||
const compaction2: CompactionEntry = {
|
|
||||||
type: "compaction",
|
|
||||||
id: "compaction2-id",
|
|
||||||
parentId: a4.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
summary: "Second summary",
|
|
||||||
firstKeptEntryId: preparation!.firstKeptEntryId,
|
|
||||||
tokensBefore: preparation!.tokensBefore,
|
|
||||||
};
|
|
||||||
const contextAfter = buildSessionContext([...pathEntries, compaction2]);
|
|
||||||
const contextAfterText = extractText(contextAfter.messages);
|
|
||||||
|
|
||||||
expect(contextAfterText).toContain("user msg 2 - kept by compaction1");
|
|
||||||
expect(contextAfterText).toContain("user msg 3 - kept by compaction1");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should re-summarize previously kept messages when the recent window moves past them", () => {
|
it("should re-summarize previously kept messages when the recent window moves past them", () => {
|
||||||
|
|||||||
@@ -67,19 +67,20 @@ function useSummaryStreamFn(harness: Harness, summary: string): () => number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seedCompactableSession(harness: Harness): void {
|
function seedCompactableSession(harness: Harness): void {
|
||||||
|
harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
harness.sessionManager.appendMessage({
|
harness.sessionManager.appendMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "message to compact" }],
|
content: [{ type: "text", text: "message to compact" }],
|
||||||
timestamp: now - 1000,
|
timestamp: now - 1000,
|
||||||
});
|
});
|
||||||
harness.sessionManager.appendMessage(
|
const assistant = createAssistant(harness, {
|
||||||
createAssistant(harness, {
|
stopReason: "stop",
|
||||||
stopReason: "stop",
|
totalTokens: 100,
|
||||||
totalTokens: 100,
|
timestamp: now - 500,
|
||||||
timestamp: now - 500,
|
});
|
||||||
}),
|
assistant.content = [{ type: "text", text: "assistant response to compact" }];
|
||||||
);
|
harness.sessionManager.appendMessage(assistant);
|
||||||
harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages;
|
harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +97,7 @@ describe("AgentSession compaction characterization", () => {
|
|||||||
|
|
||||||
it("manually compacts using an extension-provided summary", async () => {
|
it("manually compacts using an extension-provided summary", async () => {
|
||||||
const harness = await createHarness({
|
const harness = await createHarness({
|
||||||
|
settings: { compaction: { keepRecentTokens: 1 } },
|
||||||
extensionFactories: [
|
extensionFactories: [
|
||||||
(pi) => {
|
(pi) => {
|
||||||
pi.on("session_before_compact", async (event) => ({
|
pi.on("session_before_compact", async (event) => ({
|
||||||
@@ -145,7 +147,7 @@ describe("AgentSession compaction characterization", () => {
|
|||||||
|
|
||||||
const result = await harness.session.compact();
|
const result = await harness.session.compact();
|
||||||
|
|
||||||
expect(result.summary).toBe("summary from custom stream");
|
expect(result.summary).toContain("summary from custom stream");
|
||||||
expect(getStreamCallCount()).toBe(1);
|
expect(getStreamCallCount()).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,6 +167,7 @@ describe("AgentSession compaction characterization", () => {
|
|||||||
|
|
||||||
it("cancels in-progress manual compaction when abortCompaction is called", async () => {
|
it("cancels in-progress manual compaction when abortCompaction is called", async () => {
|
||||||
const harness = await createHarness({
|
const harness = await createHarness({
|
||||||
|
settings: { compaction: { keepRecentTokens: 1 } },
|
||||||
extensionFactories: [
|
extensionFactories: [
|
||||||
(pi) => {
|
(pi) => {
|
||||||
pi.on("session_before_compact", async (event) => {
|
pi.on("session_before_compact", async (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user