Fix API key priority and compaction bugs

- getEnvApiKey: ANTHROPIC_OAUTH_TOKEN now takes precedence over ANTHROPIC_API_KEY
- findCutPoint: Stop scan-backwards loop at session header (was decrementing past it causing null preparation)
- generateSummary/generateTurnPrefixSummary: Throw on stopReason=error instead of returning empty string
- Test files: Fix API key priority order, use keepRecentTokens=1 for small test conversations
This commit is contained in:
Mario Zechner 2025-12-26 00:05:02 +01:00
parent c58d5f20a4
commit 251fea752c
7 changed files with 98 additions and 110 deletions

View file

@ -23,8 +23,15 @@ export default function (pi: HookAPI) {
ctx.ui.notify("Custom compaction hook triggered", "info");
const { messagesToSummarize, messagesToKeep, previousSummary, tokensBefore, resolveApiKey, entries, signal } =
event;
const {
messagesToSummarize,
messagesToKeep,
previousSummary,
tokensBefore,
resolveApiKey,
entries: _,
signal,
} = event;
// Use Gemini Flash for summarization (cheaper/faster than most conversation models)
const model = getModel("google", "gemini-2.5-flash");

View file

@ -224,7 +224,7 @@ export function findCutPoint(
// Walk backwards from newest, accumulating estimated message sizes
let accumulatedTokens = 0;
let cutIndex = startIndex; // Default: keep everything in range
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
for (let i = endIndex - 1; i >= startIndex; i--) {
const entry = entries[i];
@ -250,8 +250,8 @@ export function findCutPoint(
// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)
while (cutIndex > startIndex) {
const prevEntry = entries[cutIndex - 1];
// Stop at compaction boundaries
if (prevEntry.type === "compaction") {
// Stop at session header or compaction boundaries
if (prevEntry.type === "session" || prevEntry.type === "compaction") {
break;
}
if (prevEntry.type === "message") {
@ -320,6 +320,10 @@ export async function generateSummary(
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
if (response.stopReason === "error") {
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
}
const textContent = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
@ -550,6 +554,10 @@ async function generateTurnPrefixSummary(
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
if (response.stopReason === "error") {
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
}
return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)

View file

@ -20,7 +20,7 @@ import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { codingTools } from "../src/core/tools/index.js";
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
describe.skipIf(!API_KEY)("AgentSession branching", () => {
let session: AgentSession;

View file

@ -20,7 +20,7 @@ import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { codingTools } from "../src/core/tools/index.js";
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
let session: AgentSession;
@ -46,7 +46,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
}
});
function createSession() {
function createSession(inMemory = false) {
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const transport = new ProviderTransport({
@ -62,8 +62,10 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
},
});
sessionManager = SessionManager.create(tempDir);
sessionManager = inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir);
const settingsManager = SettingsManager.create(tempDir, tempDir);
// Use minimal keepRecentTokens so small test conversations have something to summarize
settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
const modelRegistry = new ModelRegistry(authStorage);
@ -156,64 +158,31 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
expect(compaction.type).toBe("compaction");
if (compaction.type === "compaction") {
expect(compaction.summary.length).toBeGreaterThan(0);
// firstKeptEntryId can be 0 if all messages fit within keepRecentTokens
// (which is the case for small conversations)
expect(compaction.firstKeptEntryId).toBeGreaterThanOrEqual(0);
expect(typeof compaction.firstKeptEntryId).toBe("string");
expect(compaction.tokensBefore).toBeGreaterThan(0);
}
}, 120000);
it("should work with --no-session mode (in-memory only)", async () => {
const model = getModel("anthropic", "claude-sonnet-4-5")!;
createSession(true); // in-memory mode
const transport = new ProviderTransport({
getApiKey: () => API_KEY,
});
// Send prompts
await session.prompt("What is 2+2? Reply with just the number.");
await session.agent.waitForIdle();
const agent = new Agent({
transport,
initialState: {
model,
systemPrompt: "You are a helpful assistant. Be concise.",
tools: codingTools,
},
});
await session.prompt("What is 3+3? Reply with just the number.");
await session.agent.waitForIdle();
// Create in-memory session manager
const noSessionManager = SessionManager.inMemory();
// Compact should work even without file persistence
const result = await session.compact();
const settingsManager = SettingsManager.create(tempDir, tempDir);
const authStorage = new AuthStorage(join(tempDir, "auth.json"));
const modelRegistry = new ModelRegistry(authStorage);
expect(result.summary).toBeDefined();
expect(result.summary.length).toBeGreaterThan(0);
const noSessionSession = new AgentSession({
agent,
sessionManager: noSessionManager,
settingsManager,
modelRegistry,
});
try {
// Send prompts
await noSessionSession.prompt("What is 2+2? Reply with just the number.");
await noSessionSession.agent.waitForIdle();
await noSessionSession.prompt("What is 3+3? Reply with just the number.");
await noSessionSession.agent.waitForIdle();
// Compact should work even without file persistence
const result = await noSessionSession.compact();
expect(result.summary).toBeDefined();
expect(result.summary.length).toBeGreaterThan(0);
// In-memory entries should have the compaction
const entries = noSessionManager.getEntries();
const compactionEntries = entries.filter((e) => e.type === "compaction");
expect(compactionEntries.length).toBe(1);
} finally {
noSessionSession.dispose();
}
// In-memory entries should have the compaction
const entries = sessionManager.getEntries();
const compactionEntries = entries.filter((e) => e.type === "compaction");
expect(compactionEntries.length).toBe(1);
}, 120000);
it("should emit correct events during auto-compaction", async () => {

View file

@ -16,7 +16,7 @@ import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { codingTools } from "../src/core/tools/index.js";
const API_KEY = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN;
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
describe.skipIf(!API_KEY)("Compaction hooks", () => {
let session: AgentSession;