mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(extensions): emit compaction failed for extensions (#8241)
* fix(extensions): emit compaction failed * changelog yeet
This commit is contained in:
parent
df018b6020
commit
a6b1dbceb1
7 changed files with 139 additions and 10 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Added `session_compact_failed` extension events so compaction failures and aborts expose their reason, retry state, source, and error message to handlers ([#8175](https://github.com/earendil-works/pi/issues/8175)).
|
||||
- Fixed npm package update checks treating older registry versions as available updates, preventing `pi update` from downgrading already-newer installed packages ([#8226](https://github.com/earendil-works/pi/issues/8226)).
|
||||
- Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models in the selectable catalog ([#8167](https://github.com/earendil-works/pi/issues/8167)).
|
||||
- Fixed `pi.registerFlag()` accepting default values that do not match the declared flag type ([#8064](https://github.com/earendil-works/pi/issues/8064)).
|
||||
|
|
|
|||
|
|
@ -346,6 +346,21 @@ pi.on("session_before_compact", async (event, ctx) => {
|
|||
|
||||
See [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.
|
||||
|
||||
### session_compact_failed
|
||||
|
||||
Fired when manual or automatic compaction fails or is aborted. This is useful for telemetry extensions that need to pair `session_before_compact` attempts with terminal outcomes.
|
||||
|
||||
```typescript
|
||||
pi.on("session_compact_failed", async (event, ctx) => {
|
||||
const { reason, errorMessage, aborted, willRetry, fromExtension } = event;
|
||||
// reason - "manual" (/compact), "threshold", or "overflow"
|
||||
// errorMessage - present for non-abort failures
|
||||
// aborted - true for cancelled/aborted compactions
|
||||
// willRetry - whether the aborted turn would have retried after compaction
|
||||
// fromExtension - whether extension-provided compaction content was being used
|
||||
});
|
||||
```
|
||||
|
||||
### session_before_tree
|
||||
|
||||
Fired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.
|
||||
|
|
|
|||
|
|
@ -330,7 +330,8 @@ user sends another prompt ◄─────────────────
|
|||
|
||||
/compact or auto-compaction
|
||||
├─► session_before_compact (can cancel or customize)
|
||||
└─► session_compact
|
||||
├─► session_compact (success)
|
||||
└─► session_compact_failed (failure or abort)
|
||||
|
||||
/tree navigation
|
||||
├─► session_before_tree (can cancel or customize)
|
||||
|
|
@ -448,7 +449,7 @@ pi.on("session_before_fork", async (event, ctx) => {
|
|||
After a successful fork or clone, pi emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "fork"` and `previousSessionFile`.
|
||||
Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
|
||||
|
||||
#### session_before_compact / session_compact
|
||||
#### session_before_compact / session_compact / session_compact_failed
|
||||
|
||||
Fired on compaction. See [compaction.md](compaction.md) for details.
|
||||
|
||||
|
|
@ -479,6 +480,14 @@ pi.on("session_compact", async (event, ctx) => {
|
|||
// event.reason - "manual" (/compact), "threshold", or "overflow"
|
||||
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
|
||||
});
|
||||
|
||||
pi.on("session_compact_failed", async (event, ctx) => {
|
||||
// event.reason - "manual" (/compact), "threshold", or "overflow"
|
||||
// event.errorMessage - present for non-abort failures
|
||||
// event.aborted - true for cancelled/aborted compactions
|
||||
// event.willRetry - whether the aborted turn would have retried after compaction
|
||||
// event.fromExtension - whether extension-provided compaction content was being used
|
||||
});
|
||||
```
|
||||
|
||||
#### session_before_tree / session_tree
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ import {
|
|||
type ReplacedSessionContext,
|
||||
type SessionBeforeCompactResult,
|
||||
type SessionBeforeTreeResult,
|
||||
type SessionCompactFailedEvent,
|
||||
type SessionStartEvent,
|
||||
type ShutdownHandler,
|
||||
type ToolDefinition,
|
||||
|
|
@ -574,6 +575,12 @@ export class AgentSession {
|
|||
});
|
||||
}
|
||||
|
||||
private async _emitSessionCompactFailed(event: Omit<SessionCompactFailedEvent, "type">): Promise<void> {
|
||||
if (this._extensionRunner.hasHandlers("session_compact_failed")) {
|
||||
await this._extensionRunner.emit({ type: "session_compact_failed", ...event });
|
||||
}
|
||||
}
|
||||
|
||||
private _getIdleWaitPromise(): Promise<void> {
|
||||
if (!this._idleWaitPromise) {
|
||||
this._idleWaitPromise = new Promise((resolve) => {
|
||||
|
|
@ -1801,6 +1808,7 @@ export class AgentSession {
|
|||
await this.abort();
|
||||
this._compactionAbortController = new AbortController();
|
||||
this._emit({ type: "compaction_start", reason: "manual" });
|
||||
let fromExtension = false;
|
||||
|
||||
try {
|
||||
if (!this.model) {
|
||||
|
|
@ -1823,7 +1831,6 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
let extensionCompaction: CompactionResult | undefined;
|
||||
let fromExtension = false;
|
||||
|
||||
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
||||
const result = (await this._extensionRunner.emit({
|
||||
|
|
@ -1928,6 +1935,7 @@ export class AgentSession {
|
|||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
|
||||
const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
|
||||
this._compactionAbortController = undefined;
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
|
|
@ -1935,7 +1943,14 @@ export class AgentSession {
|
|||
result: undefined,
|
||||
aborted,
|
||||
willRetry: false,
|
||||
errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
||||
errorMessage,
|
||||
});
|
||||
await this._emitSessionCompactFailed({
|
||||
reason: "manual",
|
||||
errorMessage,
|
||||
aborted,
|
||||
willRetry: false,
|
||||
fromExtension,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
@ -2031,6 +2046,13 @@ export class AgentSession {
|
|||
willRetry: false,
|
||||
errorMessage,
|
||||
});
|
||||
await this._emitSessionCompactFailed({
|
||||
reason: "overflow",
|
||||
errorMessage,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
fromExtension: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -2088,6 +2110,7 @@ export class AgentSession {
|
|||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
let started = false;
|
||||
let fromExtension = false;
|
||||
|
||||
try {
|
||||
if (!this.model) {
|
||||
|
|
@ -2108,7 +2131,6 @@ export class AgentSession {
|
|||
started = true;
|
||||
|
||||
let extensionCompaction: CompactionResult | undefined;
|
||||
let fromExtension = false;
|
||||
|
||||
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
||||
const extensionResult = (await this._extensionRunner.emit({
|
||||
|
|
@ -2129,6 +2151,12 @@ export class AgentSession {
|
|||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
await this._emitSessionCompactFailed({
|
||||
reason,
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
fromExtension: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -2182,6 +2210,12 @@ export class AgentSession {
|
|||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
await this._emitSessionCompactFailed({
|
||||
reason,
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
fromExtension,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -2235,16 +2269,24 @@ export class AgentSession {
|
|||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||
if (started) {
|
||||
const formattedErrorMessage =
|
||||
reason === "overflow"
|
||||
? `Context overflow recovery failed: ${errorMessage}`
|
||||
: `Auto-compaction failed: ${errorMessage}`;
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
errorMessage:
|
||||
reason === "overflow"
|
||||
? `Context overflow recovery failed: ${errorMessage}`
|
||||
: `Auto-compaction failed: ${errorMessage}`,
|
||||
errorMessage: formattedErrorMessage,
|
||||
});
|
||||
await this._emitSessionCompactFailed({
|
||||
reason,
|
||||
errorMessage: formattedErrorMessage,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
fromExtension,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export type {
|
|||
SessionBeforeTreeEvent,
|
||||
SessionBeforeTreeResult,
|
||||
SessionCompactEvent,
|
||||
SessionCompactFailedEvent,
|
||||
SessionEvent,
|
||||
SessionInfoChangedEvent,
|
||||
SessionShutdownEvent,
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ export interface SessionBeforeCompactEvent {
|
|||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
/** Fired after context compaction */
|
||||
/** Fired after context compaction succeeds */
|
||||
export interface SessionCompactEvent {
|
||||
type: "session_compact";
|
||||
compactionEntry: CompactionEntry;
|
||||
|
|
@ -612,6 +612,21 @@ export interface SessionCompactEvent {
|
|||
willRetry: boolean;
|
||||
}
|
||||
|
||||
/** Fired after context compaction fails or is aborted */
|
||||
export interface SessionCompactFailedEvent {
|
||||
type: "session_compact_failed";
|
||||
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
|
||||
reason: "manual" | "threshold" | "overflow";
|
||||
/** Error text when compaction failed for a non-abort reason. */
|
||||
errorMessage?: string;
|
||||
/** True when compaction was cancelled or aborted. */
|
||||
aborted: boolean;
|
||||
/** True when the aborted turn would have been retried after this compaction (overflow recovery) */
|
||||
willRetry: boolean;
|
||||
/** True when the failing compaction content came from a session_before_compact handler. */
|
||||
fromExtension: boolean;
|
||||
}
|
||||
|
||||
/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */
|
||||
export interface SessionShutdownEvent {
|
||||
type: "session_shutdown";
|
||||
|
|
@ -658,6 +673,7 @@ export type SessionEvent =
|
|||
| SessionBeforeForkEvent
|
||||
| SessionBeforeCompactEvent
|
||||
| SessionCompactEvent
|
||||
| SessionCompactFailedEvent
|
||||
| SessionShutdownEvent
|
||||
| SessionBeforeTreeEvent
|
||||
| SessionTreeEvent;
|
||||
|
|
@ -1214,6 +1230,7 @@ export interface ExtensionAPI {
|
|||
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
||||
): void;
|
||||
on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
|
||||
on(event: "session_compact_failed", handler: ExtensionHandler<SessionCompactFailedEvent>): void;
|
||||
on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
|
||||
on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;
|
||||
on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
|
||||
|
|
|
|||
|
|
@ -278,6 +278,50 @@ describe("AgentSession compaction characterization", () => {
|
|||
expect(getStreamCallCount()).toBe(1);
|
||||
});
|
||||
|
||||
it("notifies extensions when auto-compaction fails", async () => {
|
||||
const failedEvents: Array<{
|
||||
reason: "manual" | "threshold" | "overflow";
|
||||
errorMessage?: string;
|
||||
aborted: boolean;
|
||||
willRetry: boolean;
|
||||
fromExtension: boolean;
|
||||
}> = [];
|
||||
const harness = await createHarness({
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
pi.on("session_compact_failed", async (event) => {
|
||||
failedEvents.push(event);
|
||||
});
|
||||
},
|
||||
],
|
||||
});
|
||||
harnesses.push(harness);
|
||||
seedCompactableSession(harness);
|
||||
harness.session.agent.streamFunction = () => {
|
||||
throw new Error("summary generator blew up");
|
||||
};
|
||||
const sessionInternals = harness.session as unknown as SessionWithCompactionInternals;
|
||||
|
||||
await expect(sessionInternals._runAutoCompaction("threshold", false)).resolves.toBe(false);
|
||||
|
||||
expect(harness.eventsOfType("compaction_end").at(-1)).toMatchObject({
|
||||
reason: "threshold",
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
errorMessage: "Auto-compaction failed: summary generator blew up",
|
||||
});
|
||||
expect(failedEvents).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "session_compact_failed",
|
||||
reason: "threshold",
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
fromExtension: false,
|
||||
errorMessage: "Auto-compaction failed: summary generator blew up",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("compacts and resumes after a length stop below the desired output limit", async () => {
|
||||
const harness = await createHarness({
|
||||
models: [{ id: "faux-1", contextWindow: 1000, maxTokens: 100 }],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue