diff --git a/src/hooks/anthropic-context-window-limit-recovery/message-builder.ts b/src/hooks/anthropic-context-window-limit-recovery/message-builder.ts index b2b3fd56c..f9efee09e 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/message-builder.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/message-builder.ts @@ -158,7 +158,7 @@ export async function getLastAssistant( // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any, directory: string, -): Promise | null> { +): Promise<{ info: Record; hasContent: boolean } | null> { try { const resp = await (client as Client).session.messages({ path: { id: sessionID }, @@ -175,7 +175,15 @@ export async function getLastAssistant( return info?.role === "assistant" }) if (!last) return null - return (last as { info?: Record }).info ?? null + + const message = last as SDKMessage & { info?: Record } + const info = message.info + if (!info) return null + + return { + info, + hasContent: messageHasContentFromSDK(message), + } } catch { return null } diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts index 72dd37466..f30046962 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts @@ -6,8 +6,11 @@ import * as originalLogger from "../../shared/logger" const executeCompactMock = mock(async () => {}) const getLastAssistantMock = mock(async () => ({ - providerID: "anthropic", - modelID: "claude-sonnet-4-6", + info: { + providerID: "anthropic", + modelID: "claude-sonnet-4-6", + }, + hasContent: true, })) const parseAnthropicTokenLimitErrorMock = mock(() => ({ providerID: "anthropic", @@ -115,4 +118,43 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { restore() } }) + + test("does not treat empty summary assistant messages as successful compaction", async () => { + //#given + const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks() + getLastAssistantMock.mockResolvedValueOnce({ + info: { + summary: true, + providerID: "anthropic", + modelID: "claude-sonnet-4-6", + }, + hasContent: false, + }) + const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") + const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext()) + + try { + //#when + await hook.event({ + event: { + type: "session.error", + properties: { sessionID: "session-empty-summary", error: "prompt is too long" }, + }, + }) + + await hook.event({ + event: { + type: "session.idle", + properties: { sessionID: "session-empty-summary" }, + }, + }) + + //#then + expect(getClearTimeoutCalls()).toEqual([1 as ReturnType]) + expect(executeCompactMock).toHaveBeenCalledTimes(1) + expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-empty-summary") + } finally { + restore() + } + }) }) diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.ts index 63a0f6fba..15c0ee1f2 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.ts @@ -72,8 +72,9 @@ export function createAnthropicContextWindowLimitRecoveryHook( } const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory) - const providerID = parsed.providerID ?? (lastAssistant?.providerID as string | undefined) - const modelID = parsed.modelID ?? (lastAssistant?.modelID as string | undefined) + const lastAssistantInfo = lastAssistant?.info + const providerID = parsed.providerID ?? (lastAssistantInfo?.providerID as string | undefined) + const modelID = parsed.modelID ?? (lastAssistantInfo?.modelID as string | undefined) await ctx.client.tui .showToast({ @@ -136,14 +137,15 @@ export function createAnthropicContextWindowLimitRecoveryHook( const errorData = autoCompactState.errorDataBySession.get(sessionID) const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory) + const lastAssistantInfo = lastAssistant?.info - if (lastAssistant?.summary === true) { + if (lastAssistantInfo?.summary === true && lastAssistant?.hasContent) { autoCompactState.pendingCompact.delete(sessionID) return } - const providerID = errorData?.providerID ?? (lastAssistant?.providerID as string | undefined) - const modelID = errorData?.modelID ?? (lastAssistant?.modelID as string | undefined) + const providerID = errorData?.providerID ?? (lastAssistantInfo?.providerID as string | undefined) + const modelID = errorData?.modelID ?? (lastAssistantInfo?.modelID as string | undefined) await ctx.client.tui .showToast({