> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 155ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

../coding-agent/examples/hooks/full-compaction.ts(25,16): error TS2345: Argument of type '{ role: "assistant" | "user"; content: string; }' is not assignable to parameter of type 'Message'.
  Type '{ role: "assistant" | "user"; content: string; }' is not assignable to type 'AssistantMessage | UserMessage'.
    Property 'timestamp' is missing in type '{ role: "assistant" | "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(40,50): error TS2345: Argument of type 'AppMessage[]' is not assignable to parameter of type '{ role: string; content: unknown; }[]'.
  Type 'AppMessage' is not assignable to type '{ role: string; content: unknown; }'.
    Property 'content' is missing in type 'BashExecutionMessage' but required in type '{ role: string; content: unknown; }'.
../coding-agent/examples/hooks/full-compaction.ts(57,5): error TS2322: Type '{ role: "user"; content: string; } | Message' is not assignable to type 'Message'.
  Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
    Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(58,5): error TS2322: Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
  Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/agent
npm error workspace @mariozechner/pi-agent-core@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/agent
npm error command failed
npm error command sh -c tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 40ms. No fixes applied.
../coding-agent/examples/hooks/full-compaction.ts(25,16): error TS2345: Argument of type '{ role: "assistant" | "user"; content: string; }' is not assignable to parameter of type 'Message'.
  Type '{ role: "assistant" | "user"; content: string; }' is not assignable to type 'AssistantMessage | UserMessage'.
    Property 'timestamp' is missing in type '{ role: "assistant" | "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(40,50): error TS2345: Argument of type 'AppMessage[]' is not assignable to parameter of type '{ role: string; content: unknown; }[]'.
  Type 'AppMessage' is not assignable to type '{ role: string; content: unknown; }'.
    Property 'content' is missing in type 'BashExecutionMessage' but required in type '{ role: string; content: unknown; }'.
../coding-agent/examples/hooks/full-compaction.ts(57,5): error TS2322: Type '{ role: "user"; content: string; } | Message' is not assignable to type 'Message'.
  Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
    Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(58,5): error TS2322: Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
  Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/ai
npm error workspace @mariozechner/pi-ai@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/ai
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

examples/hooks/full-compaction.ts(25,16): error TS2345: Argument of type '{ role: "assistant" | "user"; content: string; }' is not assignable to parameter of type 'Message'.
  Type '{ role: "assistant" | "user"; content: string; }' is not assignable to type 'AssistantMessage | UserMessage'.
    Property 'timestamp' is missing in type '{ role: "assistant" | "user"; content: string; }' but required in type 'UserMessage'.
examples/hooks/full-compaction.ts(40,50): error TS2345: Argument of type 'AppMessage[]' is not assignable to parameter of type '{ role: string; content: unknown; }[]'.
  Type 'AppMessage' is not assignable to type '{ role: string; content: unknown; }'.
    Property 'content' is missing in type 'BashExecutionMessage' but required in type '{ role: string; content: unknown; }'.
examples/hooks/full-compaction.ts(57,5): error TS2322: Type '{ role: "user"; content: string; } | Message' is not assignable to type 'Message'.
  Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
    Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
examples/hooks/full-compaction.ts(58,5): error TS2322: Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
  Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error workspace @mariozechner/pi-coding-agent@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error command failed
npm error command sh -c tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 16ms. No fixes applied.
../coding-agent/examples/hooks/full-compaction.ts(25,16): error TS2345: Argument of type '{ role: "assistant" | "user"; content: string; }' is not assignable to parameter of type 'Message'.
  Type '{ role: "assistant" | "user"; content: string; }' is not assignable to type 'AssistantMessage | UserMessage'.
    Property 'timestamp' is missing in type '{ role: "assistant" | "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(40,50): error TS2345: Argument of type 'AppMessage[]' is not assignable to parameter of type '{ role: string; content: unknown; }[]'.
  Type 'AppMessage' is not assignable to type '{ role: string; content: unknown; }'.
    Property 'content' is missing in type 'BashExecutionMessage' but required in type '{ role: string; content: unknown; }'.
../coding-agent/examples/hooks/full-compaction.ts(57,5): error TS2322: Type '{ role: "user"; content: string; } | Message' is not assignable to type 'Message'.
  Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
    Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(58,5): error TS2322: Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
  Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/mom
npm error workspace @mariozechner/pi-mom@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/mom
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 25ms. No fixes applied.
../coding-agent/examples/hooks/full-compaction.ts(25,16): error TS2345: Argument of type '{ role: "assistant" | "user"; content: string; }' is not assignable to parameter of type 'Message'.
  Type '{ role: "assistant" | "user"; content: string; }' is not assignable to type 'AssistantMessage | UserMessage'.
    Property 'timestamp' is missing in type '{ role: "assistant" | "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(40,50): error TS2345: Argument of type 'AppMessage[]' is not assignable to parameter of type '{ role: string; content: unknown; }[]'.
  Type 'AppMessage' is not assignable to type '{ role: string; content: unknown; }'.
    Property 'content' is missing in type 'BashExecutionMessage' but required in type '{ role: string; content: unknown; }'.
../coding-agent/examples/hooks/full-compaction.ts(57,5): error TS2322: Type '{ role: "user"; content: string; } | Message' is not assignable to type 'Message'.
  Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
    Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
../coding-agent/examples/hooks/full-compaction.ts(58,5): error TS2322: Type '{ role: "user"; content: string; }' is not assignable to type 'Message'.
  Property 'timestamp' is missing in type '{ role: "user"; content: string; }' but required in type 'UserMessage'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/tui
npm error workspace @mariozechner/pi-tui@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/tui
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit

Command exited with code 2


Let me check the Message type more carefully and look at how compaction.ts does it:


- What remains to be done (clear next steps)
- Any critical data, examples, or references needed to continue

Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;

/**
 * Generate a summary of the conversation using the LLM.
 */
export async function generateSummary(
	currentMessages: AppMessage[],
	model: Model<any>,
	reserveTokens: number,
	apiKey: string,
	signal?: AbortSignal,
	customInstructions?: string,
): Promise<string> {
	const maxTokens = Math.floor(0.8 * reserveTokens);

	const prompt = customInstructions
		? `${SUMMARIZATION_PROMPT}\n\nAdditional focus: ${customInstructions}`
		: SUMMARIZATION_PROMPT;

	// Transform custom messages (like bashExecution) to LLM-compatible messages
	const transformedMessages = messageTransformer(currentMessages);

	const summarizationMessages = [
		...transformedMessages,
		{
			role: "user" as const,
			content: [{ type: "text" as const, text: prompt }],
			timestamp: Date.now(),
		},
	];

	const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });

	const textContent = response.content
		.filter((c): c is { type: "text"; text: string } => c.type === "text")
		.map((c) => c.text)
		.join("\n");

	return textContent;
}

// ============================================================================
// Compaction Preparation (for hooks)
// ============================================================================

export interface CompactionPreparation {
	cutPoint: CutPointResult;
	messagesToSummarize: AppMessage[];
	tokensBefore: number;
	boundaryStart: number;
}

export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
	if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
		return null;
	}

	let prevCompactionIndex = -1;
	for (let i = entries.length - 1; i >= 0; i--) {
		if (entries[i].type === "compaction") {
			prevCompactionIndex = i;
			break;
		}
	}
	const boundaryStart = prevCompactionIndex + 1;
	const boundaryEnd = entries.length;

	const lastUsage = getLastAssistantUsage(entries);
	const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;

	const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);

	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
	const messagesToSummarize: AppMessage[] = [];
	for (let i = boundaryStart; i < historyEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {

[158 more lines in file. Use offset=360 to continue]


I see, it uses `messageTransformer` from `./messages.js` and `complete` from `@mariozechner/pi-ai`. Let me check how to use these:

bash

cd /Users/badlogic/workspaces/pi-mono && grep -n "export.*complete\|export.*messageTransformer" packages/ai/src/index.ts packages/coding-agent/src/index.ts 2>/dev/null | head -10


packages/coding-agent/src/index.ts:74:export { messageTransformer } from "./core/messages.js";


cd /Users/badlogic/workspaces/pi-mono && grep -n "export.*complete" packages/ai/src/index.ts


(no output)

Command exited with code 1


cd /Users/badlogic/workspaces/pi-mono && grep -n "complete" packages/ai/src/index.ts


cd /Users/badlogic/workspaces/pi-mono && grep "export" packages/ai/src/stream.ts | head -10


export function setApiKey(provider: KnownProvider, key: string): void;
export function setApiKey(provider: string, key: string): void;
export function setApiKey(provider: any, key: string): void {
export function getApiKey(provider: KnownProvider): string | undefined;
export function getApiKey(provider: string): string | undefined;
export function getApiKey(provider: any): string | undefined {
export async function resolveApiKey(provider: KnownProvider): Promise<string | undefined>;
export async function resolveApiKey(provider: string): Promise<string | undefined>;
export async function resolveApiKey(provider: any): Promise<string | undefined> {
export function stream<TApi extends Api>(


cd /Users/badlogic/workspaces/pi-mono && grep -n "export" packages/ai/src/index.ts | head -20


1:export * from "./agent/index.js";
2:export * from "./models.js";
3:export * from "./providers/anthropic.js";
4:export * from "./providers/google.js";
5:export * from "./providers/google-gemini-cli.js";
6:export * from "./providers/openai-completions.js";
7:export * from "./providers/openai-responses.js";
8:export * from "./stream.js";
9:export * from "./types.js";
10:export * from "./utils/oauth/index.js";
11:export * from "./utils/overflow.js";
12:export * from "./utils/typebox-helpers.js";
13:export * from "./utils/validation.js";


cd /Users/badlogic/workspaces/pi-mono && grep "export.*complete" packages/ai/src/stream.ts


export async function complete<TApi extends Api>(
export async function completeSimple<TApi extends Api>(


Good, so `complete` and `completeSimple` are exported from `@mariozechner/pi-ai`. And `messageTransformer` is exported from `@mariozechner/pi-coding-agent`. Let me check if it's a default export or named:

cd /Users/badlogic/workspaces/pi-mono && grep "messageTransformer" packages/coding-agent/src/core/messages.ts


 * - Agent's messageTransformer option (for prompt calls)
export function messageTransformer(messages: AppMessage[]): Message[] {


write

/**
 * Full Context Compaction Hook
 *
 * Replaces the default compaction behavior with a full summary of the entire context.
 * Instead of keeping the last 20k tokens of conversation turns, this hook:
 * 1. Summarizes ALL messages being compacted into a single comprehensive summary
 * 2. Discards all old turns completely
 *
 * This is useful when you want maximum context window space for new work
 * at the cost of losing exact conversation history.
 *
 * Usage:
 *   pi --hook examples/hooks/full-compaction.ts
 */

import { complete } from "@mariozechner/pi-ai";
import { messageTransformer } from "@mariozechner/pi-coding-agent";
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";

export default function (pi: HookAPI) {
	pi.on("session", async (event, ctx) => {
		if (event.reason !== "before_compact") return;

		const { messagesToSummarize, tokensBefore, model, apiKey, cutPoint } = event;

		ctx.ui.notify(`Compacting ${tokensBefore.toLocaleString()} tokens with full summary...`, "info");

		// Transform app messages to LLM-compatible format
		const transformedMessages = messageTransformer(messagesToSummarize);

		// Build messages that ask for a comprehensive summary
		const summaryMessages = [
			...transformedMessages,
			{
				role: "user" as const,
				content: [
					{
						type: "text" as const,
						text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:

1. The main goals and objectives discussed
2. Key decisions made and their rationale
3. Important code changes, file modifications, or technical details
4. Current state of any ongoing work
5. Any blockers, issues, or open questions
6. Next steps that were planned or suggested

Be thorough but concise. The summary will replace the entire conversation history, so include all information needed to continue the work effectively.

Format the summary as structured markdown with clear sections.`,
					},
				],
				timestamp: Date.now(),
			},
		];

		try {
			// Use the same model and API key that would be used for compaction
			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });

			const summary = response.content
				.filter((c): c is { type: "text"; text: string } => c.type === "text")
				.map((c) => c.text)
				.join("\n");

			if (!summary.trim()) {
				ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
				return; // Fall back to default compaction
			}

			// Return a compaction entry that discards ALL old messages
			// firstKeptEntryIndex points to after all summarized content
			return {
				compactionEntry: {
					type: "compaction" as const,
					timestamp: new Date().toISOString(),
					summary,
					firstKeptEntryIndex: cutPoint.firstKeptEntryIndex,
					tokensBefore,
				},
			};
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			ctx.ui.notify(`Compaction failed: ${message}`, "error");
			// Fall back to default compaction on error
			return;
		}
	});
}


Successfully wrote 3074 bytes to /Users/badlogic/workspaces/pi-mono/packages/coding-agent/examples/hooks/full-compaction.ts


bash

cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1


> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 186ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 46ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 17ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 27ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit


Now let me update the README for hooks to include this new example:


# Hooks Examples

Example hooks for pi-coding-agent.

## Examples

### permission-gate.ts
Prompts for confirmation before running dangerous bash commands (rm -rf, sudo, chmod 777, etc.).

### git-checkpoint.ts
Creates git stash checkpoints at each turn, allowing code restoration when branching.

### protected-paths.ts
Blocks writes to protected paths (.env, .git/, node_modules/).

### file-trigger.ts
Watches a trigger file and injects its contents into the conversation. Useful for external systems (CI, file watchers, webhooks) to send messages to the agent.

### confirm-destructive.ts
Prompts for confirmation before destructive session actions (clear, switch, branch). Demonstrates how to cancel `before_*` session events.

### dirty-repo-guard.ts
Prevents session changes when there are uncommitted git changes. Blocks clear/switch/branch until you commit.

### auto-commit-on-exit.ts
Automatically commits changes when the agent exits (shutdown event). Uses the last assistant message to generate a commit message.

## Usage

```bash
# Test directly
pi --hook examples/hooks/permission-gate.ts

# Or copy to hooks directory for persistent use
cp permission-gate.ts ~/.pi/agent/hooks/
```

## Writing Hooks

See [docs/hooks.md](../../docs/hooks.md) for full documentation.

[56 more lines in file. Use offset=41 to continue]


edit

### auto-commit-on-exit.ts
Automatically commits changes when the agent exits (shutdown event). Uses the last assistant message to generate a commit message.

## Usage

### auto-commit-on-exit.ts
Automatically commits changes when the agent exits (shutdown event). Uses the last assistant message to generate a commit message.

### full-compaction.ts
Custom context compaction that summarizes the entire conversation instead of keeping recent turns. Uses the `before_compact` hook event to intercept compaction and generate a comprehensive summary using `complete()` from the AI package. Useful when you want maximum context window space at the cost of losing exact conversation history.

## Usage


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/examples/hooks/README.md. Changed 167 characters to 528 characters.

    ...
 24
 25 ### auto-commit-on-exit.ts
 26 Automatically commits changes when the agent exits (shutdown event). Uses the last assistant message to generate a commit message.
 27
+28 ### full-compaction.ts
+29 Custom context compaction that summarizes the entire conversation instead of keeping recent turns. Uses the `before_compact` hook event to intercept compaction and generate a comprehensive summary using `complete()` from the AI package. Useful when you want maximum context window space at the cost of losing exact conversation history.
+30
 28 ## Usage
 29
 30 ```bash
 31 # Test directly
    ...


bash

cd /Users/badlogic/workspaces/pi-mono && git add -A && git commit -m "Add full-compaction hook example" && git push


Running formatting, linting, and type checking...

> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 155ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 41ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 17ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 4ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 26ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit

✅ All pre-commit checks passed!
[main 5aa53de] Add full-compaction hook example
 3 files changed, 138 insertions(+), 46 deletions(-)
 create mode 100644 packages/coding-agent/examples/hooks/full-compaction.ts
To https://github.com/badlogic/pi-mono.git
   bf313d2..5aa53de  main -> main


Done. Added `full-compaction.ts` hook example that:

1. Intercepts the `before_compact` session event
2. Uses `messageTransformer` to convert app messages to LLM format
3. Calls `complete()` with the model and API key provided in the event
4. Generates a comprehensive summary of the entire conversation
5. Returns a custom `compactionEntry` that replaces all old messages with the summary


so i think we need to pass resolveApiKey to before_compaction hooks as well, so they have an easier time invoking models via complete


Good point. Currently the hook receives `apiKey` as a string, but if the hook wants to call a different model or needs to resolve keys for other purposes, it would need access to the resolver.

Let me check how the before_compact event is emitted:

cd /Users/badlogic/workspaces/pi-mono && grep -n "before_compact" packages/coding-agent/src/core/agent-session.ts


768:					reason: "before_compact",
918:					reason: "before_compact",


			let fromHook = false;

			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
				})) as SessionEventResult | undefined;

				if (result?.cancel) {
					throw new Error("Compaction cancelled");
				}

				if (result?.compactionEntry) {
					compactionEntry = result.compactionEntry;
					fromHook = true;
				}
			}

			if (!compactionEntry) {
				compactionEntry = await compact(
					entries,
					this.model,
					settings,
					apiKey,
					this._compactionAbortController.signal,
					customInstructions,
				);
			}

			if (this._compactionAbortController.signal.aborted) {
				throw new Error("Compaction cancelled");

[768 more lines in file. Use offset=800 to continue]


Now let me check the types:


	| (SessionEventBase & {
			reason: "branch" | "before_branch";
			/** Index of the turn to branch from */
			targetTurnIndex: number;
	  })
	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			messagesToSummarize: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			apiKey: string;
	  })
	| (SessionEventBase & {
			reason: "compact";
			compactionEntry: CompactionEntry;
			tokensBefore: number;
			/** Whether the compaction entry was provided by a hook */
			fromHook: boolean;
	  });

/**
 * Event data for agent_start event.
 * Fired when an agent loop starts (once per user prompt).

[250 more lines in file. Use offset=150 to continue]


I need to add `resolveApiKey` to the `before_compact` event. Let me update the types and the emit locations:

edit

	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			messagesToSummarize: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			apiKey: string;
	  })

	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			messagesToSummarize: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			apiKey: string;
			/** Resolve API key for any model (checks settings, OAuth, env vars) */
			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
	  })


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/hooks/types.ts. Changed 224 characters to 369 characters.

     ...
 134 			tokensBefore: number;
 135 			customInstructions?: string;
 136 			model: Model<any>;
 137 			apiKey: string;
+138 			/** Resolve API key for any model (checks settings, OAuth, env vars) */
+139 			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
 138 	  })
 139 	| (SessionEventBase & {
 140 			reason: "compact";
 141 			compactionEntry: CompactionEntry;
     ...


Now update the agent-session.ts to pass resolveApiKey:


			let fromHook = false;

			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
				})) as SessionEventResult | undefined;

				if (result?.cancel) {
					throw new Error("Compaction cancelled");
				}

				if (result?.compactionEntry) {
					compactionEntry = result.compactionEntry;
					fromHook = true;
				}
			}

			if (!compactionEntry) {
				compactionEntry = await compact(
					entries,

[778 more lines in file. Use offset=790 to continue]


			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
				})) as SessionEventResult | undefined;

			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts. Changed 481 characters to 522 characters.

      ...
  771 					tokensBefore: preparation.tokensBefore,
  772 					customInstructions,
  773 					model: this.model,
  774 					apiKey,
+ 775 					resolveApiKey: this._resolveApiKey,
  775 				})) as SessionEventResult | undefined;
  776
  777 				if (result?.cancel) {
  778 					throw new Error("Compaction cancelled");
      ...


Now find the other location (auto-compaction):


			if (!preparation) {
				this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
				return;
			}

			let compactionEntry: CompactionEntry | undefined;
			let fromHook = false;

			if (this._hookRunner?.hasHandlers("session")) {
				const hookResult = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					apiKey,
				})) as SessionEventResult | undefined;

				if (hookResult?.cancel) {
					this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
					return;
				}

				if (hookResult?.compactionEntry) {
					compactionEntry = hookResult.compactionEntry;
					fromHook = true;
				}
			}

			if (!compactionEntry) {
				compactionEntry = await compact(
					entries,
					this.model,
					settings,
					apiKey,

[624 more lines in file. Use offset=945 to continue]


			if (this._hookRunner?.hasHandlers("session")) {
				const hookResult = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					apiKey,
				})) as SessionEventResult | undefined;

			if (this._hookRunner?.hasHandlers("session")) {
				const hookResult = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts. Changed 496 characters to 537 characters.

      ...
  922 					tokensBefore: preparation.tokensBefore,
  923 					customInstructions: undefined,
  924 					model: this.model,
  925 					apiKey,
+ 926 					resolveApiKey: this._resolveApiKey,
  926 				})) as SessionEventResult | undefined;
  927
  928 				if (hookResult?.cancel) {
  929 					this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
      ...


Now run check:


> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 166ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 38ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 16ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 25ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit


Now update the docs:

bash

cd /Users/badlogic/workspaces/pi-mono && grep -n "before_compact" packages/coding-agent/docs/hooks.md


135:  ├─► session (reason: "before_compact", can cancel or provide custom summary)
155:  //               "before_branch" | "branch" | "before_compact" | "compact" | "shutdown"
176:- `before_compact` / `compact`: Context compaction (auto or `/compact`)
181:For `before_compact` events, additional fields are available:
637:    if (event.reason !== "before_compact") return;


For `before_branch` and `branch` events, `event.targetTurnIndex` contains the entry index being branched from.

For `before_compact` events, additional fields are available:
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
- `event.messagesToSummarize`: Messages that will be summarized
- `event.tokensBefore`: Current context token count
- `event.model`: Model to use for summarization
- `event.apiKey`: API key for the model
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)

Return `{ compactionEntry }` to provide a custom summary instead of the default. The `compactionEntry` must have: `type: "compaction"`, `timestamp`, `summary`, `firstKeptEntryIndex` (from `cutPoint`), `tokensBefore`.

For `compact` events (after compaction):
- `event.compactionEntry`: The saved compaction entry
- `event.tokensBefore`: Token count before compaction
- `event.fromHook`: Whether the compaction entry was provided by a hook

### agent_start / agent_end

Fired once per user prompt.

```typescript
pi.on("agent_start", async (event, ctx) => {});

pi.on("agent_end", async (event, ctx) => {
  // event.messages: AppMessage[] - new messages from this prompt
});
```

[622 more lines in file. Use offset=208 to continue]


edit

For `before_compact` events, additional fields are available:
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
- `event.messagesToSummarize`: Messages that will be summarized
- `event.tokensBefore`: Current context token count
- `event.model`: Model to use for summarization
- `event.apiKey`: API key for the model
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)

For `before_compact` events, additional fields are available:
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
- `event.messagesToSummarize`: Messages that will be summarized
- `event.tokensBefore`: Current context token count
- `event.model`: Model to use for summarization
- `event.apiKey`: API key for the model
- `event.resolveApiKey`: Function to resolve API key for any model (checks settings, OAuth, env vars)
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/hooks.md. Changed 444 characters to 546 characters.

     ...
 183 - `event.messagesToSummarize`: Messages that will be summarized
 184 - `event.tokensBefore`: Current context token count
 185 - `event.model`: Model to use for summarization
 186 - `event.apiKey`: API key for the model
+187 - `event.resolveApiKey`: Function to resolve API key for any model (checks settings, OAuth, env vars)
 187 - `event.customInstructions`: Optional custom focus for summary (from `/compact` command)
 188
 189 Return `{ compactionEntry }` to provide a custom summary instead of the default. The `compactionEntry` must have: `type: "compaction"`, `timestamp`, `summary`, `firstKeptEntryIndex` (from `cutPoint`), `tokensBefore`.
 190
     ...


Now update the changelog:


# Changelog

## [Unreleased]

## [0.27.5] - 2025-12-24

### Added

- **HTML export syntax highlighting**: Code blocks in markdown and tool outputs (read, write) now have syntax highlighting using highlight.js with theme-aware colors matching the TUI.
- **HTML export improvements**: Render markdown server-side using marked (tables, headings, code blocks, etc.), honor user's chosen theme (light/dark), add image rendering for user messages, and style code blocks with TUI-like language markers. ([@scutifer](https://github.com/scutifer))

### Fixed

- **Ghostty inline images in tmux**: Fixed terminal detection for Ghostty when running inside tmux by checking `GHOSTTY_RESOURCES_DIR` env var. ([#299](https://github.com/badlogic/pi-mono/pull/299) by [@nicobailon](https://github.com/nicobailon))

## [0.27.4] - 2025-12-24

### Fixed

- **Symlinked skill directories**: Skills in symlinked directories (e.g., `~/.pi/agent/skills/my-skills -> /path/to/skills`) are now correctly discovered and loaded.

[825 more lines in file. Use offset=21 to continue]


## [Unreleased]

### Added

- **Compaction hook `resolveApiKey`**: The `before_compact` session event now includes `resolveApiKey` function to resolve API keys for any model (checks settings, OAuth, env vars). Useful for hooks that need to call different models during custom compaction.

## [0.27.5] - 2025-12-24


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md. Changed 41 characters to 313 characters.

   1 # Changelog
   2
   3 ## [Unreleased]
   4
+  5 ### Added
+  6
+  7 - **Compaction hook `resolveApiKey`**: The `before_compact` session event now includes `resolveApiKey` function to resolve API keys for any model (checks settings, OAuth, env vars). Useful for hooks that need to call different models during custom compaction.
+  8
   5 ## [0.27.5] - 2025-12-24
   6
   7 ### Added
   8
     ...


bash

cd /Users/badlogic/workspaces/pi-mono && git add -A && git commit -m "Add resolveApiKey to before_compact hook event" && git push


Running formatting, linting, and type checking...

> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 163ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 44ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 19ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 26ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit

✅ All pre-commit checks passed!
[main 43a5447] Add resolveApiKey to before_compact hook event
 4 files changed, 9 insertions(+)
To https://github.com/badlogic/pi-mono.git
   5aa53de..43a5447  main -> main


Done. Added `resolveApiKey` to the `before_compact` hook event. Now hooks can easily resolve API keys for any model they want to use during custom compaction, not just the current model.


can you adjust the full-compaction example to use resolveApiKey instead?


edit

		const { messagesToSummarize, tokensBefore, model, apiKey, cutPoint } = event;

		ctx.ui.notify(`Compacting ${tokensBefore.toLocaleString()} tokens with full summary...`, "info");

		// Transform app messages to LLM-compatible format
		const transformedMessages = messageTransformer(messagesToSummarize);

		// Build messages that ask for a comprehensive summary
		const summaryMessages = [
			...transformedMessages,
			{
				role: "user" as const,
				content: [
					{
						type: "text" as const,
						text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:

1. The main goals and objectives discussed
2. Key decisions made and their rationale
3. Important code changes, file modifications, or technical details
4. Current state of any ongoing work
5. Any blockers, issues, or open questions
6. Next steps that were planned or suggested

Be thorough but concise. The summary will replace the entire conversation history, so include all information needed to continue the work effectively.

Format the summary as structured markdown with clear sections.`,
					},
				],
				timestamp: Date.now(),
			},
		];

		try {
			// Use the same model and API key that would be used for compaction
			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });

		const { messagesToSummarize, tokensBefore, model, resolveApiKey, cutPoint } = event;

		ctx.ui.notify(`Compacting ${tokensBefore.toLocaleString()} tokens with full summary...`, "info");

		// Resolve API key for the model
		const apiKey = await resolveApiKey(model);
		if (!apiKey) {
			ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
			return;
		}

		// Transform app messages to LLM-compatible format
		const transformedMessages = messageTransformer(messagesToSummarize);

		// Build messages that ask for a comprehensive summary
		const summaryMessages = [
			...transformedMessages,
			{
				role: "user" as const,
				content: [
					{
						type: "text" as const,
						text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:

1. The main goals and objectives discussed
2. Key decisions made and their rationale
3. Important code changes, file modifications, or technical details
4. Current state of any ongoing work
5. Any blockers, issues, or open questions
6. Next steps that were planned or suggested

Be thorough but concise. The summary will replace the entire conversation history, so include all information needed to continue the work effectively.

Format the summary as structured markdown with clear sections.`,
					},
				],
				timestamp: Date.now(),
			},
		];

		try {
			// Use the same model with resolved API key
			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/examples/hooks/full-compaction.ts. Changed 1349 characters to 1536 characters.

    ...
 20 export default function (pi: HookAPI) {
 21 	pi.on("session", async (event, ctx) => {
 22 		if (event.reason !== "before_compact") return;
 23
-24 		const { messagesToSummarize, tokensBefore, model, apiKey, cutPoint } = event;
+24 		const { messagesToSummarize, tokensBefore, model, resolveApiKey, cutPoint } = event;
 25
 26 		ctx.ui.notify(`Compacting ${tokensBefore.toLocaleString()} tokens with full summary...`, "info");
 27
+28 		// Resolve API key for the model
+29 		const apiKey = await resolveApiKey(model);
+30 		if (!apiKey) {
+31 			ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
+32 			return;
+33 		}
+34
 28 		// Transform app messages to LLM-compatible format
 29 		const transformedMessages = messageTransformer(messagesToSummarize);
 30
 31 		// Build messages that ask for a comprehensive summary
 32 		const summaryMessages = [
 33 			...transformedMessages,
 34 			{
 35 				role: "user" as const,
 36 				content: [
 37 					{
 38 						type: "text" as const,
 39 						text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:
 40
 41 1. The main goals and objectives discussed
 42 2. Key decisions made and their rationale
 43 3. Important code changes, file modifications, or technical details
 44 4. Current state of any ongoing work
 45 5. Any blockers, issues, or open questions
 46 6. Next steps that were planned or suggested
 47
 48 Be thorough but concise. The summary will replace the entire conversation history, so include all information needed to continue the work effectively.
 49
 50 Format the summary as structured markdown with clear sections.`,
 51 					},
 52 				],
 53 				timestamp: Date.now(),
 54 			},
 55 		];
 56
 57 		try {
-58 			// Use the same model and API key that would be used for compaction
+65 			// Use the same model with resolved API key
 59 			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });
 60
 61 			const summary = response.content
 62 				.filter((c): c is { type: "text"; text: string } => c.type === "text")
    ...


> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 156ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 46ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 19ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 25ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit


bash

cd /Users/badlogic/workspaces/pi-mono && git add -A && git commit -m "Update full-compaction example to use resolveApiKey" && git push


Running formatting, linting, and type checking...

> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 155ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 42ms. No fixes applied.

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 16ms. No fixes applied.

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 25ms. No fixes applied.

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit

✅ All pre-commit checks passed!
[main e428329] Update full-compaction example to use resolveApiKey
 1 file changed, 9 insertions(+), 2 deletions(-)
To https://github.com/badlogic/pi-mono.git
   43a5447..e428329  main -> main


Done. Updated the example to use `resolveApiKey` instead of the pre-resolved `apiKey` string.


firstKeptEntryIndex: cutPoint.firstKeptEntryIndex,

so, assume we have (t == turn)

t
t
t
t <- firstKeptEntryIndex
t
t

this is passed to before_compact. then we compact

t
t
t
t <- firstKeptEntryIndex
t
t
c (compaction entry)

this doesn't set firstkeptEntryIndex correctly, we'd want it to point to c, no? thou our "replay" logic for compaction will have to account for that.


You're right. Let me trace through the logic to understand what `firstKeptEntryIndex` should mean:

cd /Users/badlogic/workspaces/pi-mono && grep -n "firstKeptEntryIndex" packages/coding-agent/src/core/session-manager.ts | head -20


46:	firstKeptEntryIndex: number;
150:	for (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {


			if (entry.type === "message") {
				messages.push(entry.message);
			}
		}
		return { messages, thinkingLevel, model };
	}

	const compactionEvent = entries[latestCompactionIndex] as CompactionEntry;

	const keptMessages: AppMessage[] = [];
	for (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			keptMessages.push(entry.message);
		}
	}

	const messages: AppMessage[] = [];
	messages.push(createSummaryMessage(compactionEvent.summary));
	messages.push(...keptMessages);

	return { messages, thinkingLevel, model };
}

function getSessionDirectory(cwd: string, agentDir: string): string {
	const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
	const sessionDir = join(agentDir, "sessions", safePath);
	if (!existsSync(sessionDir)) {
		mkdirSync(sessionDir, { recursive: true });
	}

[331 more lines in file. Use offset=170 to continue]


I see. The `firstKeptEntryIndex` is used during replay to know which entries to load after the compaction. So:

```
0: t
1: t
2: t
3: t <- firstKeptEntryIndex (these are kept)
4: t
5: t
6: c (compaction entry with firstKeptEntryIndex=3)
```

When replaying, it loads the summary from `c`, then loads entries from index 3 onwards (which includes the compaction entry itself at index 6).

Wait, that's wrong. Let me re-read:

```typescript
for (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {
```

So if `firstKeptEntryIndex=3`, it loads entries 3, 4, 5, 6 (including compaction entry at 6). But compaction entries aren't messages, so they're skipped in the loop.

The issue is: the default compaction keeps some recent turns. But in the full-compaction example, we want to summarize everything and keep nothing (except what comes after). So `firstKeptEntryIndex` should point to the index after all the summarized content.

Looking at the example:
```typescript
firstKeptEntryIndex: cutPoint.firstKeptEntryIndex,
```

This is the default behavior. But for "full compaction" we'd want `firstKeptEntryIndex` to point past all messages being summarized, effectively discarding them all.

Actually wait, let me check what `cutPoint.firstKeptEntryIndex` represents:


export interface CutPointResult {
	/** Index of first entry to keep */
	firstKeptEntryIndex: number;
	/** Index of user message that starts the turn being split, or -1 if not splitting */
	turnStartIndex: number;
	/** Whether this cut splits a turn (cut point is not a user message) */
	isSplitTurn: boolean;
}

/**
 * Find the cut point in session entries that keeps approximately `keepRecentTokens`.
 *
 * Algorithm: Walk backwards from newest, accumulating estimated message sizes.
 * Stop when we've accumulated >= keepRecentTokens. Cut at that point.
 *
 * Can cut at user OR assistant messages (never tool results). When cutting at an
 * assistant message with tool calls, its tool results come after and will be kept.
 *
 * Returns CutPointResult with:
 * - firstKeptEntryIndex: the entry index to start keeping from
 * - turnStartIndex: if cutting mid-turn, the user message that started that turn
 * - isSplitTurn: whether we're cutting in the middle of a turn
 *
 * Only considers entries between `startIndex` and `endIndex` (exclusive).
 */
export function findCutPoint(
	entries: SessionEntry[],
	startIndex: number,
	endIndex: number,
	keepRecentTokens: number,

[307 more lines in file. Use offset=211 to continue]


So `cutPoint.firstKeptEntryIndex` is "Index of first entry to keep" based on the `keepRecentTokens` setting. If you want to discard everything and only keep the summary, you'd want `firstKeptEntryIndex` to point past all the content being summarized.

Looking at the hook event:

```typescript
messagesToSummarize: preparation.messagesToSummarize,
```

This is the messages from `boundaryStart` to `historyEnd`. Let me check what those values are:


	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
	const messagesToSummarize: AppMessage[] = [];
	for (let i = boundaryStart; i < historyEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			messagesToSummarize.push(entry.message);
		}
	}

	return { cutPoint, messagesToSummarize, tokensBefore, boundaryStart };
}

// ============================================================================
// Main compaction function
// ============================================================================

const TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn.
This is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.

Create a handoff summary that captures:
- What the user originally asked for in this turn
- Key decisions and progress made early in this turn
- Important context needed to understand the kept suffix

Be concise. Focus on information needed to understand the retained recent work.`;

/**
 * Calculate compaction and generate summary.
 * Returns the CompactionEntry to append to the session file.
 *
 * @param entries - All session entries
 * @param model - Model to use for summarization
 * @param settings - Compaction settings
 * @param apiKey - API key for LLM
 * @param signal - Optional abort signal
 * @param customInstructions - Optional custom focus for the summary
 */
export async function compact(
	entries: SessionEntry[],
	model: Model<any>,

[123 more lines in file. Use offset=395 to continue]


I see. So `messagesToSummarize` contains messages from `boundaryStart` to `historyEnd`, and `cutPoint.firstKeptEntryIndex` points to where to start keeping entries.

For the "full compaction" example, if we want to discard everything and just keep the summary, we should probably not pass `cutPoint.firstKeptEntryIndex` but rather the index that points past all the messages we summarized.

But wait, there's also the issue that `messagesToSummarize` only contains messages up to `historyEnd`, not ALL messages. So even the default behavior keeps some recent turns.

For true "full compaction", we'd want to:
1. Summarize ALL messages (not just `messagesToSummarize`)
2. Set `firstKeptEntryIndex` to point to the current end of entries

Let me check what's available in the event:

cd /Users/badlogic/workspaces/pi-mono && grep -n "entries" packages/coding-agent/src/core/hooks/types.ts | head -10


98:	/** All session entries (including pre-compaction history) */
99:	entries: SessionEntry[];


The hook has access to `entries`. So for true full compaction, the hook could:

1. Extract ALL messages from `entries` (not just `messagesToSummarize`)
2. Summarize them all
3. Set `firstKeptEntryIndex` to `entries.length` (past everything)

But the example currently just uses `messagesToSummarize` which is only part of the context. You're right that the example name is misleading. It doesn't do "full" compaction, it just replaces the default summarization with a custom one for the same subset.

Do you want me to fix the example to actually do full compaction (summarize everything, keep nothing)?


- should we remove apiKey and instead have users always use resolveApiKey? i think that's better
- for the example hook:
  - we can add the messages to keep to messagesToSummarize (messagesToSummarize must be a defensive copy when passed to before_compact, maybe messagesToKeep) should also be in before_compact?, then the hook can do messges = [..messagesToSummarize, ..messagesToKeep] to get all messages (after the last compaction point)?


Good points. Let me think through this:

1. **Remove `apiKey`, only use `resolveApiKey`**: Makes sense. The hook can call `resolveApiKey(model)` to get the same key, and has flexibility to resolve keys for other models.

2. **Add `messagesToKeep` to `before_compact`**: Yes, this would let hooks see the full picture:
   - `messagesToSummarize`: Messages that will be summarized (discarded after summary)
   - `messagesToKeep`: Messages that will be kept as-is after the summary
   - Hook can do `[...messagesToSummarize, ...messagesToKeep]` to get all messages since last compaction

For `firstKeptEntryIndex` in the hook's returned `compactionEntry`:
- If hook returns its own summary and wants to keep `messagesToKeep`, use `cutPoint.firstKeptEntryIndex`
- If hook wants full compaction (discard everything), use `entries.length` (or we could add a simpler field)

Let me implement these changes:


			targetTurnIndex: number;
	  })
	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			messagesToSummarize: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			apiKey: string;
			/** Resolve API key for any model (checks settings, OAuth, env vars) */
			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
	  })
	| (SessionEventBase & {
			reason: "compact";
			compactionEntry: CompactionEntry;
			tokensBefore: number;
			/** Whether the compaction entry was provided by a hook */
			fromHook: boolean;
	  });

[254 more lines in file. Use offset=148 to continue]


edit

	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			messagesToSummarize: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			apiKey: string;
			/** Resolve API key for any model (checks settings, OAuth, env vars) */
			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
	  })

	| (SessionEventBase & {
			reason: "before_compact";
			cutPoint: CutPointResult;
			/** Messages that will be summarized and discarded */
			messagesToSummarize: AppMessage[];
			/** Messages that will be kept after the summary (recent turns) */
			messagesToKeep: AppMessage[];
			tokensBefore: number;
			customInstructions?: string;
			model: Model<any>;
			/** Resolve API key for any model (checks settings, OAuth, env vars) */
			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
	  })


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/hooks/types.ts. Changed 369 characters to 510 characters.

     ...
 129 	  })
 130 	| (SessionEventBase & {
 131 			reason: "before_compact";
 132 			cutPoint: CutPointResult;
+133 			/** Messages that will be summarized and discarded */
 133 			messagesToSummarize: AppMessage[];
+135 			/** Messages that will be kept after the summary (recent turns) */
+136 			messagesToKeep: AppMessage[];
 134 			tokensBefore: number;
 135 			customInstructions?: string;
 136 			model: Model<any>;
-137 			apiKey: string;
 138 			/** Resolve API key for any model (checks settings, OAuth, env vars) */
 139 			resolveApiKey: (model: Model<any>) => Promise<string | undefined>;
 140 	  })
 141 	| (SessionEventBase & {
     ...


Now update compaction.ts to return `messagesToKeep` in the preparation:


// Compaction Preparation (for hooks)
// ============================================================================

export interface CompactionPreparation {
	cutPoint: CutPointResult;
	messagesToSummarize: AppMessage[];
	tokensBefore: number;
	boundaryStart: number;
}

export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
	if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
		return null;
	}

	let prevCompactionIndex = -1;
	for (let i = entries.length - 1; i >= 0; i--) {
		if (entries[i].type === "compaction") {
			prevCompactionIndex = i;
			break;
		}
	}
	const boundaryStart = prevCompactionIndex + 1;
	const boundaryEnd = entries.length;

	const lastUsage = getLastAssistantUsage(entries);
	const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;

	const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);

	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
	const messagesToSummarize: AppMessage[] = [];
	for (let i = boundaryStart; i < historyEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			messagesToSummarize.push(entry.message);
		}
	}

	return { cutPoint, messagesToSummarize, tokensBefore, boundaryStart };
}

// ============================================================================
// Main compaction function
// ============================================================================

[148 more lines in file. Use offset=370 to continue]


export interface CompactionPreparation {
	cutPoint: CutPointResult;
	messagesToSummarize: AppMessage[];
	tokensBefore: number;
	boundaryStart: number;
}

export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
	if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
		return null;
	}

	let prevCompactionIndex = -1;
	for (let i = entries.length - 1; i >= 0; i--) {
		if (entries[i].type === "compaction") {
			prevCompactionIndex = i;
			break;
		}
	}
	const boundaryStart = prevCompactionIndex + 1;
	const boundaryEnd = entries.length;

	const lastUsage = getLastAssistantUsage(entries);
	const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;

	const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);

	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
	const messagesToSummarize: AppMessage[] = [];
	for (let i = boundaryStart; i < historyEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			messagesToSummarize.push(entry.message);
		}
	}

	return { cutPoint, messagesToSummarize, tokensBefore, boundaryStart };
}

export interface CompactionPreparation {
	cutPoint: CutPointResult;
	/** Messages that will be summarized and discarded */
	messagesToSummarize: AppMessage[];
	/** Messages that will be kept after the summary (recent turns) */
	messagesToKeep: AppMessage[];
	tokensBefore: number;
	boundaryStart: number;
}

export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
	if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
		return null;
	}

	let prevCompactionIndex = -1;
	for (let i = entries.length - 1; i >= 0; i--) {
		if (entries[i].type === "compaction") {
			prevCompactionIndex = i;
			break;
		}
	}
	const boundaryStart = prevCompactionIndex + 1;
	const boundaryEnd = entries.length;

	const lastUsage = getLastAssistantUsage(entries);
	const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;

	const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);

	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;

	// Messages to summarize (will be discarded after summary)
	const messagesToSummarize: AppMessage[] = [];
	for (let i = boundaryStart; i < historyEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			messagesToSummarize.push(entry.message);
		}
	}

	// Messages to keep (recent turns, kept after summary)
	const messagesToKeep: AppMessage[] = [];
	for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
		const entry = entries[i];
		if (entry.type === "message") {
			messagesToKeep.push(entry.message);
		}
	}

	return { cutPoint, messagesToSummarize, messagesToKeep, tokensBefore, boundaryStart };
}


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/compaction.ts. Changed 1234 characters to 1740 characters.

     ...
 326 // ============================================================================
 327
 328 export interface CompactionPreparation {
 329 	cutPoint: CutPointResult;
+330 	/** Messages that will be summarized and discarded */
 330 	messagesToSummarize: AppMessage[];
+332 	/** Messages that will be kept after the summary (recent turns) */
+333 	messagesToKeep: AppMessage[];
 331 	tokensBefore: number;
 332 	boundaryStart: number;
 333 }
 334
 335 export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
 336 	if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
 337 		return null;
 338 	}
 339
 340 	let prevCompactionIndex = -1;
 341 	for (let i = entries.length - 1; i >= 0; i--) {
 342 		if (entries[i].type === "compaction") {
 343 			prevCompactionIndex = i;
 344 			break;
 345 		}
 346 	}
 347 	const boundaryStart = prevCompactionIndex + 1;
 348 	const boundaryEnd = entries.length;
 349
 350 	const lastUsage = getLastAssistantUsage(entries);
 351 	const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
 352
 353 	const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
 354
 355 	const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
+359
+360 	// Messages to summarize (will be discarded after summary)
 356 	const messagesToSummarize: AppMessage[] = [];
 357 	for (let i = boundaryStart; i < historyEnd; i++) {
 358 		const entry = entries[i];
 359 		if (entry.type === "message") {
 360 			messagesToSummarize.push(entry.message);
 361 		}
 362 	}
 363
-364 	return { cutPoint, messagesToSummarize, tokensBefore, boundaryStart };
+369 	// Messages to keep (recent turns, kept after summary)
+370 	const messagesToKeep: AppMessage[] = [];
+371 	for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
+372 		const entry = entries[i];
+373 		if (entry.type === "message") {
+374 			messagesToKeep.push(entry.message);
+375 		}
+376 	}
+377
+378 	return { cutPoint, messagesToSummarize, messagesToKeep, tokensBefore, boundaryStart };
 365 }
 366
 367 // ============================================================================
 368 // Main compaction function
     ...


Now update agent-session.ts to pass `messagesToKeep` and remove `apiKey`:


			let fromHook = false;

			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;

				if (result?.cancel) {
					throw new Error("Compaction cancelled");
				}

				if (result?.compactionEntry) {
					compactionEntry = result.compactionEntry;
					fromHook = true;
				}
			}

			if (!compactionEntry) {
				compactionEntry = await compact(
					entries,
					this.model,
					settings,
					apiKey,
					this._compactionAbortController.signal,

[775 more lines in file. Use offset=795 to continue]


			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;

			if (this._hookRunner?.hasHandlers("session")) {
				const result = (await this._hookRunner.emit({
					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: [...preparation.messagesToSummarize],
					messagesToKeep: [...preparation.messagesToKeep],
					tokensBefore: preparation.tokensBefore,
					customInstructions,
					model: this.model,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts. Changed 522 characters to 568 characters.

      ...
  766 					sessionFile: this.sessionFile,
  767 					previousSessionFile: null,
  768 					reason: "before_compact",
  769 					cutPoint: preparation.cutPoint,
- 770 					messagesToSummarize: preparation.messagesToSummarize,
+ 770 					messagesToSummarize: [...preparation.messagesToSummarize],
+ 771 					messagesToKeep: [...preparation.messagesToKeep],
  771 					tokensBefore: preparation.tokensBefore,
  772 					customInstructions,
  773 					model: this.model,
- 774 					apiKey,
  775 					resolveApiKey: this._resolveApiKey,
  776 				})) as SessionEventResult | undefined;
  777
  778 				if (result?.cancel) {
      ...


Now the second location (auto-compaction):


					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;

				if (hookResult?.cancel) {
					this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
					return;
				}

				if (hookResult?.compactionEntry) {
					compactionEntry = hookResult.compactionEntry;
					fromHook = true;
				}
			}

			if (!compactionEntry) {
				compactionEntry = await compact(
					entries,
					this.model,
					settings,

[625 more lines in file. Use offset=945 to continue]


					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: preparation.messagesToSummarize,
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					apiKey,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;

					type: "session",
					entries,
					sessionFile: this.sessionFile,
					previousSessionFile: null,
					reason: "before_compact",
					cutPoint: preparation.cutPoint,
					messagesToSummarize: [...preparation.messagesToSummarize],
					messagesToKeep: [...preparation.messagesToKeep],
					tokensBefore: preparation.tokensBefore,
					customInstructions: undefined,
					model: this.model,
					resolveApiKey: this._resolveApiKey,
				})) as SessionEventResult | undefined;


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts. Changed 432 characters to 478 characters.

      ...
  917 					sessionFile: this.sessionFile,
  918 					previousSessionFile: null,
  919 					reason: "before_compact",
  920 					cutPoint: preparation.cutPoint,
- 921 					messagesToSummarize: preparation.messagesToSummarize,
+ 921 					messagesToSummarize: [...preparation.messagesToSummarize],
+ 922 					messagesToKeep: [...preparation.messagesToKeep],
  922 					tokensBefore: preparation.tokensBefore,
  923 					customInstructions: undefined,
  924 					model: this.model,
- 925 					apiKey,
  926 					resolveApiKey: this._resolveApiKey,
  927 				})) as SessionEventResult | undefined;
  928
  929 				if (hookResult?.cancel) {
      ...


Now update the docs:

For `before_compact` events, additional fields are available:
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
- `event.messagesToSummarize`: Messages that will be summarized
- `event.tokensBefore`: Current context token count
- `event.model`: Model to use for summarization
- `event.apiKey`: API key for the model
- `event.resolveApiKey`: Function to resolve API key for any model (checks settings, OAuth, env vars)
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)

For `before_compact` events, additional fields are available:
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
- `event.messagesToSummarize`: Messages that will be summarized and discarded
- `event.messagesToKeep`: Messages that will be kept after the summary (recent turns)
- `event.tokensBefore`: Current context token count
- `event.model`: Model to use for summarization
- `event.resolveApiKey`: Function to resolve API key for any model (checks settings, OAuth, env vars)
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)

To get all messages since the last compaction: `[...event.messagesToSummarize, ...event.messagesToKeep]`


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/hooks.md. Changed 546 characters to 712 characters.

     ...
 179 For `before_branch` and `branch` events, `event.targetTurnIndex` contains the entry index being branched from.
 180
 181 For `before_compact` events, additional fields are available:
 182 - `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
-183 - `event.messagesToSummarize`: Messages that will be summarized
+183 - `event.messagesToSummarize`: Messages that will be summarized and discarded
+184 - `event.messagesToKeep`: Messages that will be kept after the summary (recent turns)
 184 - `event.tokensBefore`: Current context token count
 185 - `event.model`: Model to use for summarization
-186 - `event.apiKey`: API key for the model
 187 - `event.resolveApiKey`: Function to resolve API key for any model (checks settings, OAuth, env vars)
 188 - `event.customInstructions`: Optional custom focus for summary (from `/compact` command)
 189
+190 To get all messages since the last compaction: `[...event.messagesToSummarize, ...event.messagesToKeep]`
+191
 190 Return `{ compactionEntry }` to provide a custom summary instead of the default. The `compactionEntry` must have: `type: "compaction"`, `timestamp`, `summary`, `firstKeptEntryIndex` (from `cutPoint`), `tokensBefore`.
 191
 192 For `compact` events (after compaction):
 193 - `event.compactionEntry`: The saved compaction entry
     ...


Now update the full-compaction example to use the new API:


/**
 * Full Context Compaction Hook
 *
 * Replaces the default compaction behavior with a full summary of the entire context.
 * Instead of keeping the last 20k tokens of conversation turns, this hook:
 * 1. Summarizes ALL messages being compacted into a single comprehensive summary
 * 2. Discards all old turns completely
 *
 * This is useful when you want maximum context window space for new work
 * at the cost of losing exact conversation history.
 *
 * Usage:
 *   pi --hook examples/hooks/full-compaction.ts
 */

import { complete } from "@mariozechner/pi-ai";
import { messageTransformer } from "@mariozechner/pi-coding-agent";
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";

export default function (pi: HookAPI) {
	pi.on("session", async (event, ctx) => {
		if (event.reason !== "before_compact") return;

		const { messagesToSummarize, tokensBefore, model, resolveApiKey, cutPoint } = event;

		ctx.ui.notify(`Compacting ${tokensBefore.toLocaleString()} tokens with full summary...`, "info");

		// Resolve API key for the model
		const apiKey = await resolveApiKey(model);
		if (!apiKey) {
			ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
			return;
		}

		// Transform app messages to LLM-compatible format
		const transformedMessages = messageTransformer(messagesToSummarize);

		// Build messages that ask for a comprehensive summary
		const summaryMessages = [
			...transformedMessages,
			{
				role: "user" as const,
				content: [
					{
						type: "text" as const,
						text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:

1. The main goals and objectives discussed
2. Key decisions made and their rationale
3. Important code changes, file modifications, or technical details
4. Current state of any ongoing work
5. Any blockers, issues, or open questions
6. Next steps that were planned or suggested

Be thorough but concise. The summary will replace the entire conversation history, so include all information needed to continue the work effectively.

Format the summary as structured markdown with clear sections.`,
					},
				],
				timestamp: Date.now(),
			},
		];

		try {
			// Use the same model with resolved API key
			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });

			const summary = response.content
				.filter((c): c is { type: "text"; text: string } => c.type === "text")
				.map((c) => c.text)
				.join("\n");

			if (!summary.trim()) {
				ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
				return; // Fall back to default compaction
			}

			// Return a compaction entry that discards ALL old messages
			// firstKeptEntryIndex points to after all summarized content
			return {
				compactionEntry: {
					type: "compaction" as const,
					timestamp: new Date().toISOString(),
					summary,
					firstKeptEntryIndex: cutPoint.firstKeptEntryIndex,
					tokensBefore,
				},
			};
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			ctx.ui.notify(`Compaction failed: ${message}`, "error");
			// Fall back to default compaction on error
			return;
		}
	});
}


write

/**
 * Full Context Compaction Hook
 *
 * Replaces the default compaction behavior with a full summary of the entire context.
 * Instead of keeping the last 20k tokens of conversation turns, this hook:
 * 1. Summarizes ALL messages (both messagesToSummarize and messagesToKeep)
 * 2. Discards all old turns completely, keeping only the summary
 *
 * This is useful when you want maximum context window space for new work
 * at the cost of losing exact conversation history.
 *
 * Usage:
 *   pi --hook examples/hooks/full-compaction.ts
 */

import { complete } from "@mariozechner/pi-ai";
import { messageTransformer } from "@mariozechner/pi-coding-agent";
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";

export default function (pi: HookAPI) {
	pi.on("session", async (event, ctx) => {
		if (event.reason !== "before_compact") return;

		const { messagesToSummarize, messagesToKeep, tokensBefore, model, resolveApiKey, entries } = event;

		// Combine all messages for full summary
		const allMessages = [...messagesToSummarize, ...messagesToKeep];

		ctx.ui.notify(`Full compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens)...`, "info");

		// Resolve API key for the model
		const apiKey = await resolveApiKey(model);
		if (!apiKey) {
			ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
			return;
		}

		// Transform app messages to LLM-compatible format
		const transformedMessages = messageTransformer(allMessages);

		// Build messages that ask for a comprehensive summary
		const summaryMessages = [
			...transformedMessages,
			{
				role: "user" as const,
				content: [
					{
						type: "text" as const,
						text: `You are a conversation summarizer. Create a comprehensive summary of this entire conversation that captures:

1. The main goals and objectives discussed
2. Key decisions made and their rationale
3. Important code changes, file modifications, or technical details
4. Current state of any ongoing work
5. Any blockers, issues, or open questions
6. Next steps that were planned or suggested

Be thorough but concise. The summary will replace the ENTIRE conversation history, so include all information needed to continue the work effectively.

Format the summary as structured markdown with clear sections.`,
					},
				],
				timestamp: Date.now(),
			},
		];

		try {
			// Use the same model with resolved API key
			const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192 });

			const summary = response.content
				.filter((c): c is { type: "text"; text: string } => c.type === "text")
				.map((c) => c.text)
				.join("\n");

			if (!summary.trim()) {
				ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
				return; // Fall back to default compaction
			}

			// Return a compaction entry that discards ALL messages
			// firstKeptEntryIndex points past all current entries
			return {
				compactionEntry: {
					type: "compaction" as const,
					timestamp: new Date().toISOString(),
					summary,
					firstKeptEntryIndex: entries.length,
					tokensBefore,
				},
			};
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			ctx.ui.notify(`Compaction failed: ${message}`, "error");
			// Fall back to default compaction on error
			return;
		}
	});
}


Successfully wrote 3414 bytes to /Users/badlogic/workspaces/pi-mono/packages/coding-agent/examples/hooks/full-compaction.ts


> pi-monorepo@0.0.3 check
> biome check --write . && npm run check --workspaces --if-present && tsgo --noEmit

Checked 286 files in 155ms. No fixes applied.

> @mariozechner/pi-agent-core@0.27.5 check
> tsgo --noEmit

../coding-agent/test/compaction-hooks-example.test.ts(21,26): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(134,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(345,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
../coding-agent/test/compaction-hooks.test.ts(346,16): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/agent
npm error workspace @mariozechner/pi-agent-core@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/agent
npm error command failed
npm error command sh -c tsgo --noEmit

> @mariozechner/pi-ai@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 44 files in 38ms. No fixes applied.
../coding-agent/test/compaction-hooks-example.test.ts(21,26): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(134,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(345,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
../coding-agent/test/compaction-hooks.test.ts(346,16): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/ai
npm error workspace @mariozechner/pi-ai@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/ai
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi-coding-agent@0.27.5 check
> tsgo --noEmit && tsgo -p tsconfig.examples.json

test/compaction-hooks-example.test.ts(21,26): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
test/compaction-hooks.test.ts(134,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
test/compaction-hooks.test.ts(345,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
test/compaction-hooks.test.ts(346,16): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error workspace @mariozechner/pi-coding-agent@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error command failed
npm error command sh -c tsgo --noEmit && tsgo -p tsconfig.examples.json

> @mariozechner/pi-mom@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 16 files in 17ms. No fixes applied.
../coding-agent/test/compaction-hooks-example.test.ts(21,26): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(134,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(345,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
../coding-agent/test/compaction-hooks.test.ts(346,16): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/mom
npm error workspace @mariozechner/pi-mom@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/mom
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi@0.27.5 check
> biome check --write .

Checked 10 files in 17ms. No fixes applied.

> @mariozechner/pi-proxy@0.27.5 check
> biome check --write .

Checked 3 files in 3ms. No fixes applied.

> @mariozechner/pi-tui@0.27.5 check
> biome check --write . && tsgo --noEmit

Checked 27 files in 25ms. No fixes applied.
../coding-agent/test/compaction-hooks-example.test.ts(21,26): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(134,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; }'.
../coding-agent/test/compaction-hooks.test.ts(345,23): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
../coding-agent/test/compaction-hooks.test.ts(346,16): error TS2339: Property 'apiKey' does not exist on type 'SessionEventBase & { reason: "before_compact"; cutPoint: CutPointResult; messagesToSummarize: AppMessage[]; messagesToKeep: AppMessage[]; tokensBefore: number; customInstructions?: string | undefined; model: Model<...>; resolveApiKey: (model: Model<...>) => Promise<...>; } & { ...; }'.
  Property 'apiKey' does not exist on type 'never'.
npm error Lifecycle script `check` failed with error:
npm error code 2
npm error path /Users/badlogic/workspaces/pi-mono/packages/tui
npm error workspace @mariozechner/pi-tui@0.27.5
npm error location /Users/badlogic/workspaces/pi-mono/packages/tui
npm error command failed
npm error command sh -c biome check --write . && tsgo --noEmit

> @mariozechner/pi-web-ui@0.27.5 check
> tsgo --noEmit && cd example && tsgo --noEmit

> pi-web-ui-example@1.15.5 check
> tsgo --noEmit

Command exited with code 2


Need to update the tests:

bash

cd /Users/badlogic/workspaces/pi-mono && grep -n "apiKey" packages/coding-agent/test/compaction-hooks-example.test.ts packages/coding-agent/test/compaction-hooks.test.ts


packages/coding-agent/test/compaction-hooks-example.test.ts:21:				const apiKey = event.apiKey;
packages/coding-agent/test/compaction-hooks-example.test.ts:28:				expect(typeof apiKey).toBe("string");
packages/coding-agent/test/compaction-hooks.test.ts:134:			expect(beforeEvent.apiKey).toBeDefined();
packages/coding-agent/test/compaction-hooks.test.ts:345:		expect(typeof event.apiKey).toBe("string");
packages/coding-agent/test/compaction-hooks.test.ts:346:		expect(event.apiKey.length).toBeGreaterThan(0);


				// After narrowing, these should all be accessible
				const messages = event.messagesToSummarize;
				const cutPoint = event.cutPoint;
				const tokensBefore = event.tokensBefore;
				const model = event.model;
				const apiKey = event.apiKey;

				// Verify types
				expect(Array.isArray(messages)).toBe(true);
				expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
				expect(typeof tokensBefore).toBe("number");
				expect(model).toBeDefined();
				expect(typeof apiKey).toBe("string");

				const summary = messages
					.filter((m) => m.role === "user")
					.map((m) => `- ${typeof m.content === "string" ? m.content.slice(0, 100) : "[complex]"}`)
					.join("\n");

				const compactionEntry: CompactionEntry = {
					type: "compaction",
					timestamp: new Date().toISOString(),
					summary: `User requests:\n${summary}`,
					firstKeptEntryIndex: event.cutPoint.firstKeptEntryIndex,

[32 more lines in file. Use offset=40 to continue]


edit

				// After narrowing, these should all be accessible
				const messages = event.messagesToSummarize;
				const cutPoint = event.cutPoint;
				const tokensBefore = event.tokensBefore;
				const model = event.model;
				const apiKey = event.apiKey;

				// Verify types
				expect(Array.isArray(messages)).toBe(true);
				expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
				expect(typeof tokensBefore).toBe("number");
				expect(model).toBeDefined();
				expect(typeof apiKey).toBe("string");

				// After narrowing, these should all be accessible
				const messages = event.messagesToSummarize;
				const messagesToKeep = event.messagesToKeep;
				const cutPoint = event.cutPoint;
				const tokensBefore = event.tokensBefore;
				const model = event.model;
				const resolveApiKey = event.resolveApiKey;

				// Verify types
				expect(Array.isArray(messages)).toBe(true);
				expect(Array.isArray(messagesToKeep)).toBe(true);
				expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
				expect(typeof tokensBefore).toBe("number");
				expect(model).toBeDefined();
				expect(typeof resolveApiKey).toBe("function");


Successfully replaced text in /Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/compaction-hooks-example.test.ts. Changed 504 characters to 630 characters.

    ...
 14 				if (event.reason !== "before_compact") return;
 15
 16 				// After narrowing, these should all be accessible
 17 				const messages = event.messagesToSummarize;
+18 				const messagesToKeep = event.messagesToKeep;
 18 				const cutPoint = event.cutPoint;
 19 				const tokensBefore = event.tokensBefore;
 20 				const model = event.model;
-21 				const apiKey = event.apiKey;
+22 				const resolveApiKey = event.resolveApiKey;
 22
 23 				// Verify types
 24 				expect(Array.isArray(messages)).toBe(true);
+26 				expect(Array.isArray(messagesToKeep)).toBe(true);
 25 				expect(typeof cutPoint.firstKeptEntryIndex).toBe("number");
 26 				expect(typeof tokensBefore).toBe("number");
 27 				expect(model).toBeDefined();
-28 				expect(typeof apiKey).toBe("string");
+30 				expect(typeof resolveApiKey).toBe("function");
 29
 30 				const summary = messages
 31 					.filter((m) => m.role === "user")
 32 					.map((m) => `- ${typeof m.content === "string" ? m.content.slice(0, 100) : "[complex]"}`)
    ...


		expect(compactEvents.length).toBe(1);

		const beforeEvent = beforeCompactEvents[0];
		if (beforeEvent.reason === "before_compact") {
			expect(beforeEvent.cutPoint).toBeDefined();
			expect(beforeEvent.cutPoint.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
			expect(beforeEvent.messagesToSummarize).toBeDefined();
			expect(beforeEvent.tokensBefore).toBeGreaterThanOrEqual(0);
			expect(beforeEvent.model).toBeDefined();
			expect(beforeEvent.apiKey).toBeDefined();
		}

		const afterEvent = compactEvents[0];
		if (afterEvent.reason === "compact") {
			expect(afterEvent.compactionEntry).toBeDefined();
			expect(afterEvent.compactionEntry.summary.length).toBeGreaterThan(0);
			expect(afterEvent.tokensBefore).toBeGreaterThanOrEqual(0);
			expect(afterEvent.fromHook).toBe(false);
		}
	}, 120000);

[236 more lines in file. Use offset=145 to continue]